diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
new file mode 100644
index 0000000..08a515a
--- /dev/null
+++ b/.github/workflows/windows.yml
@@ -0,0 +1,55 @@
+name: windows
+
+on:
+ push:
+ tags:
+ - "v*windows*"
+ - "v*win*"
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ dist:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.24"
+ cache: true
+
+ - name: Установить mingw-w64 кросс-тулчейн
+ run: sudo apt-get update && sudo apt-get install -y gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 zip
+
+ - name: Собрать движок winws2.exe
+ run: ./scripts/build-engine-windows.sh
+
+ - name: Проверка Go-кода
+ run: |
+ test -z "$(gofmt -l cmd internal)"
+ go vet ./...
+ go test ./...
+ GOOS=windows go vet ./...
+
+ - name: Собрать fdpi.exe и дистрибутив
+ run: make dist-windows
+
+ - name: Отладочные артефакты в CI
+ if: github.event_name == 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/')
+ uses: actions/upload-artifact@v4
+ with:
+ name: fdpi-windows-amd64
+ path: build/fdpi-windows-amd64.zip
+
+ - name: Создать релиз
+ if: startsWith(github.ref, 'refs/tags/')
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh release create "${{ github.ref_name }}" \
+ build/fdpi-windows-amd64.zip \
+ --title "${{ github.ref_name }} — Windows" \
+ --generate-notes
diff --git a/.gitignore b/.gitignore
index d2bb77c..04321bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
/build/
+build/
/fdpi
*.test
diff --git a/Makefile b/Makefile
index 060a908..f2ae6a0 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build check engine install uninstall netns bench dist
+.PHONY: build check engine engine-windows install uninstall netns bench dist dist-windows
build:
mkdir -p build
go build -trimpath -o build/fdpi ./cmd/fdpi
@@ -9,6 +9,8 @@ check:
go test -race ./...
engine:
./scripts/build-engine.sh
+engine-windows:
+ ./scripts/build-engine-windows.sh
install:
./scripts/install.sh
uninstall:
@@ -20,3 +22,10 @@ bench:
dist:
mkdir -p build
tar --exclude=./build --exclude=./.git -czf build/fdpi-0.4.0.tar.gz --transform='s,^\.,fdpi-0.4.0,' .
+dist-windows: engine-windows
+ GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o build/windows/fdpi.exe ./cmd/fdpi
+ cp third_party/windivert/x64/WinDivert64.sys build/windows/
+ cp configs/default.json build/windows/config.json
+ rm -f build/fdpi-windows-amd64.zip
+ cd build/windows && { command -v zip >/dev/null && zip -qr ../fdpi-windows-amd64.zip fdpi.exe winws2.exe WinDivert.dll WinDivert64.sys config.json lua ZAPRET-LICENSE engine.commit ../../README_WINDOWS.md || 7z a -tzip -r ../fdpi-windows-amd64.zip fdpi.exe winws2.exe WinDivert.dll WinDivert64.sys config.json lua ZAPRET-LICENSE engine.commit ../../README_WINDOWS.md >/dev/null; }
+ @echo "Готово: build/fdpi-windows-amd64.zip"
diff --git a/README.md b/README.md
index 6a792d2..049f118 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
-
-
[](docs/INSTALL_RU.md)
[](go.mod)
[](LICENSE)
@@ -19,8 +17,8 @@
╰─────────────────────────────────────────────────────╯
```
-FDPI — терминальная утилита для Linux с русским интерфейсом, интерактивной
-установкой и отдельной фоновой службой. Локальные изменения применяются явно.
+FDPI — терминальная утилита для Linux и Windows с русским интерфейсом,
+интерактивной установкой и отдельной фоновой службой. Локальные изменения применяются явно.
В интерфейсе нет выдуманных PASS: выбранный профиль и проверенный доступ —
разные состояния.
@@ -34,7 +32,7 @@ FDPI — терминальная утилита для Linux с русским
Изображения интерфейса сняты с работающего TUI в PTY и отрисованы из терминальных
-ячеек. Баннер — иллюстрация, созданная с помощью генерации изображений.
+ячеек; на Windows отрисовка та же.
## Что такое DPI и зачем нужен FDPI
@@ -56,6 +54,8 @@ DPI анализирует пакеты и состояние соединени
## Установка
+### Linux (CachyOS / Arch)
+
```bash
git clone https://github.com/futureUnd1ground/fdpi.git
cd fdpi
@@ -76,6 +76,30 @@ fdpi config enable spotify on
fdpi apply # применить выбранные профили
```
+Conntrack, автозапуск systemd, обновление и PKGBUILD —
+[полная Linux-инструкция](docs/INSTALL_RU.md).
+
+### Windows 10/11 (x64)
+
+Сборка не нужна: скачайте `fdpi-windows-amd64.zip` из
+[релизов](https://github.com/futureUnd1ground/fdpi/releases) и распакуйте в
+отдельную папку, например `C:\FDPI`. Внутри — `fdpi.exe`, `winws2.exe`,
+подписанный драйвер WinDivert 2.2.2, Lua-библиотеки и конфигурация по умолчанию.
+
+```powershell
+Expand-Archive fdpi-windows-amd64.zip -DestinationPath C:\FDPI
+cd C:\FDPI
+.\fdpi.exe
+```
+
+`fdpi.exe` открывает мастер первого запуска: проверка компонентов, выбор
+сервисов (YouTube, Discord, Steam, Spotify) и стратегии, затем установка
+службы Windows с автозапуском — один запрос UAC — и тест выбранных доменов.
+Движок — Windows-вариант nfqws2 (zapret2) поверх WinDivert; интерфейс тот же.
+[Требования, управление службой, пути и удаление →](README_WINDOWS.md)
+
+Клавиши TUI одинаковые на обеих платформах:
+
| Клавиши | Действие |
|---|---|
| `Tab`, `←`, `→` | Разделы |
@@ -133,12 +157,14 @@ make netns # user/network namespace, настоящий движок
# FDPI — Fast DPI Bypass
**Первый запуск:** выполните `fdpi setup` (интерактивный мастер), нажмите `i` в TUI или откройте
-[установку по шагам](docs/INSTALL_RU.md). Интерфейс и основные сообщения — на русском.
+[установку по шагам](docs/INSTALL_RU.md); для Windows — [отдельную инструкцию](README_WINDOWS.md).
+Интерфейс и основные сообщения — на русском.
-FDPI 0.4.0 — Linux-утилита на Go: непривилегированный TUI, CLI и отдельный
-сервис управления **настоящим nfqws2**. Основная платформа — CachyOS/Arch Linux.
-Обработка пакетов выполняется nfqws2, а не Go. Это первая реализация, ещё не
+FDPI 0.4.0 — утилита на Go: на Linux она управляет **настоящим nfqws2**
+(NFQUEUE/nftables, systemd), на Windows 10/11 x64 — **winws2** из zapret2
+(WinDivert, служба). Основная платформа — CachyOS/Arch Linux. Обработка пакетов
+выполняется внешним движком, а не Go. Это первая реализация, ещё не
подтверждённая для ежедневной работы на разных провайдерах.
**Проверено:** сборка, race detector, реальный nftables/NFQUEUE/nfqws2 на ядре
diff --git a/README_WINDOWS.md b/README_WINDOWS.md
new file mode 100644
index 0000000..53436e1
--- /dev/null
+++ b/README_WINDOWS.md
@@ -0,0 +1,84 @@
+# FDPI для Windows
+
+FDPI управляет доменными стратегиями обхода DPI: выбирает сервисы, подбирает
+стратегии и следит за фоновой службой. На Windows вместо nfqueue/nftables
+используется движок **winws2** (zapret2, WinDivert) — те же стратегии
+(фрагментация, fake-пакеты, QUIC), тот же TUI.
+
+## Быстрый старт
+
+1. Скачайте `fdpi-windows-amd64.zip` из [релизов](../../releases) и распакуйте
+ в отдельную папку, например `C:\FDPI`. Архив уже содержит всё необходимое:
+ `fdpi.exe`, `winws2.exe`, подписанный драйвер `WinDivert64.sys`,
+ `WinDivert.dll`, Lua-библиотеки стратегий и конфигурацию по умолчанию.
+2. Запустите `fdpi.exe` (двойной клик в `cmd`/Windows Terminal — нужен
+ терминал). Откроется мастер первого запуска:
+ - проверка компонентов;
+ - выбор сервисов (YouTube, Discord, Steam, Spotify) и стратегии;
+ - установка службы Windows (одно согласие UAC) и её запуск;
+ - тест доступности выбранных доменов.
+3. Дальше — основной TUI: обзор, сервисы, стратегии, **подбор стратегии**,
+ журнал, настройки. Клавиши: `j`/`k` или стрелки, `Enter`, `q` — выход.
+
+Служба `fdpi` стартует автоматически при входе в систему. Обработка идёт
+только для доменов включённых профилей и только первых пакетов соединений
+на портах 80/443 — остальной трафик не затрагивается.
+
+## Требования
+
+- Windows 10/11 (x64). Драйвер WinDivert 2.2.2 подписан и ставится
+ автоматически при первом запуске службы; отдельной установки не требуется.
+- Права администратора — только для установки/управления службой (UAC);
+ сам интерфейс работает без них.
+
+## Управление из командной строки
+
+```
+fdpi интерфейс (TUI)
+fdpi setup мастер первого запуска заново
+fdpi start|stop|restart управление службой (с UAC)
+fdpi status [--json] состояние службы и движка
+fdpi test ДОМЕН DNS, TCP, TLS, HTTPS
+fdpi lab СЕРВИС подбор стратегии (перебор с проверкой домена)
+fdpi logs [--follow] журнал движка (engine.log)
+fdpi doctor диагностика
+fdpi service-install установить службу с автозапуском
+fdpi service-remove остановить и удалить службу
+```
+
+Настройки пользователя (черновик): `%APPDATA%\fdpi\config.json`.
+Системные настройки службы: `%ProgramData%\FDPI\config.json`,
+журнал: `%ProgramData%\FDPI\logs\engine.log`.
+
+## Как это работает
+
+1. Служба Windows запускает `fdpi daemon`, тот — `winws2.exe` с фильтром
+ WinDivert на исходящий TCP/80,443 и UDP/443.
+2. Для TLS ClientHello и HTTP-запросов к доменам из включённых профилей
+ движок применяет выбранную стратегию: подменные пакеты (`fake` с
+ искажённым sequence/TTL), фрагментация (`multisplit`), фрагментация с
+ перестановкой (`multidisorder`), QUIC Initial fake (`quic`).
+3. Домены второго уровня из hostlist покрывают поддомены автоматически.
+
+Движок winws2 — это Windows-вариант nfq2 из [zapret2](https://github.com/bol-van/zapret2)
+(подробности в `docs/ENGINE.md`), собранный кросс-компиляцией mingw-w64
+скриптом `scripts/build-engine-windows.sh`. WinDivert — отдельный проект
+basil00 (LGPL/GPL), бинарные файлы лежат в `third_party/windivert/`.
+
+## Сборка из исходников (Linux)
+
+```
+make dist-windows # build/windows + fdpi-windows-amd64.zip
+```
+
+Нужны: Go 1.24+, mingw-w64 (`x86_64-w64-mingw32-gcc`), curl, zip.
+Скрипт сам скачает Lua 5.4 и zlib, применит патч к zapret2 на закреплённый
+commit и соберёт `winws2.exe`.
+
+## Удаление
+
+```
+fdpi service-remove
+```
+Удалите папку с программой. Пользовательские настройки в `%APPDATA%\fdpi`
+и `%ProgramData%\FDPI` можно удалить вручную — они не влияют на систему.
diff --git a/cmd/fdpi/bench_test.go b/cmd/fdpi/bench_linux_test.go
similarity index 100%
rename from cmd/fdpi/bench_test.go
rename to cmd/fdpi/bench_linux_test.go
diff --git a/cmd/fdpi/logs_common.go b/cmd/fdpi/logs_common.go
new file mode 100644
index 0000000..882f339
--- /dev/null
+++ b/cmd/fdpi/logs_common.go
@@ -0,0 +1,14 @@
+package main
+
+import "fmt"
+
+func errGrepArg() error {
+ return fmt.Errorf("после --grep укажите текст поиска")
+}
+func errCategoryArg() error { return fmt.Errorf("--category err|warning|info") }
+func errBadCategory() error {
+ return fmt.Errorf("неверная категория журнала: используйте err, warning или info")
+}
+func errUnknownLogArg() error {
+ return fmt.Errorf("неизвестный параметр журнала")
+}
diff --git a/cmd/fdpi/logs_linux.go b/cmd/fdpi/logs_linux.go
new file mode 100644
index 0000000..15edf76
--- /dev/null
+++ b/cmd/fdpi/logs_linux.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+ "context"
+ "os"
+ "os/exec"
+)
+
+// runLogs показывает журнал службы через journalctl.
+func runLogs(ctx context.Context, args []string) error {
+ a := []string{"-u", "fdpi.service", "--no-pager", "-n", "200"}
+ for i := 0; i < len(args); i++ {
+ switch args[i] {
+ case "--follow":
+ a = append(a, "--follow")
+ case "--grep":
+ i++
+ if i >= len(args) {
+ return errGrepArg()
+ }
+ a = append(a, "--grep", args[i])
+ case "--category":
+ i++
+ if i >= len(args) {
+ return errCategoryArg()
+ }
+ switch args[i] {
+ case "err", "warning", "info":
+ a = append(a, "--priority", args[i])
+ default:
+ return errBadCategory()
+ }
+ default:
+ return errUnknownLogArg()
+ }
+ }
+ c := exec.CommandContext(ctx, "/usr/bin/journalctl", a...)
+ c.Stdout = os.Stdout
+ c.Stderr = os.Stderr
+ return c.Run()
+}
diff --git a/cmd/fdpi/logs_windows.go b/cmd/fdpi/logs_windows.go
new file mode 100644
index 0000000..25f2f3b
--- /dev/null
+++ b/cmd/fdpi/logs_windows.go
@@ -0,0 +1,108 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/futureUnd1ground/fdpi/internal/service"
+)
+
+// runLogs читает файл журнала движка; --follow — поллинг новых строк.
+// Фильтры --grep и --category работают как поиск по тексту записи.
+func runLogs(ctx context.Context, args []string) error {
+ follow := false
+ grep := ""
+ category := ""
+ for i := 0; i < len(args); i++ {
+ switch args[i] {
+ case "--follow":
+ follow = true
+ case "--grep":
+ i++
+ if i >= len(args) {
+ return errGrepArg()
+ }
+ grep = strings.ToLower(args[i])
+ case "--category":
+ i++
+ if i >= len(args) {
+ return errCategoryArg()
+ }
+ switch args[i] {
+ case "err", "warning", "info":
+ category = args[i]
+ default:
+ return errBadCategory()
+ }
+ default:
+ return errUnknownLogArg()
+ }
+ }
+ match := func(line string) bool {
+ l := strings.ToLower(line)
+ if grep != "" && !strings.Contains(l, grep) {
+ return false
+ }
+ switch category {
+ case "err":
+ return strings.Contains(l, "err") || strings.Contains(l, "error") || strings.Contains(l, "fatal")
+ case "warning":
+ return strings.Contains(l, "warn")
+ case "info":
+ return true
+ }
+ return true
+ }
+ path := service.LogPath()
+ if !follow {
+ b, e := os.ReadFile(path)
+ if e != nil {
+ if os.IsNotExist(e) {
+ fmt.Println("журнал пуст; служба ещё не запускалась")
+ return nil
+ }
+ return e
+ }
+ lines := strings.Split(strings.TrimRight(string(b), "\n"), "\n")
+ if len(lines) > 200 {
+ lines = lines[len(lines)-200:]
+ }
+ for _, l := range lines {
+ if match(l) {
+ fmt.Println(l)
+ }
+ }
+ return nil
+ }
+ // --follow: поллинг файла раз в 500 мс до отмены контекста (Ctrl+C).
+ var offset int64
+ for {
+ st, e := os.Stat(path)
+ if e == nil && st.Size() > offset {
+ f, fe := os.Open(path)
+ if fe == nil {
+ _, _ = f.Seek(offset, 0)
+ sc := bufio.NewScanner(f)
+ sc.Buffer(make([]byte, 64*1024), 1024*1024)
+ for sc.Scan() {
+ if match(sc.Text()) {
+ fmt.Println(sc.Text())
+ }
+ }
+ offset, _ = f.Seek(0, 1)
+ f.Close()
+ }
+ } else if e != nil && !os.IsNotExist(e) {
+ return e
+ }
+ select {
+ case <-ctx.Done():
+ return nil
+ case <-time.After(500 * time.Millisecond):
+ }
+ }
+}
diff --git a/cmd/fdpi/main.go b/cmd/fdpi/main.go
index f9b8d3a..f93bbb7 100644
--- a/cmd/fdpi/main.go
+++ b/cmd/fdpi/main.go
@@ -12,7 +12,6 @@ import (
"github.com/futureUnd1ground/fdpi/internal/tui"
"net"
"os"
- "os/exec"
"os/signal"
"path/filepath"
"strings"
@@ -50,28 +49,14 @@ func userConfig() (config.Config, error) {
}
return c, e
}
-func elevated(ctx context.Context, args ...string) error {
- exe, e := os.Executable()
- if e != nil {
- return e
- }
- if os.Geteuid() == 0 {
- return run(ctx, args)
- }
- c := exec.CommandContext(ctx, "sudo", append([]string{"--", exe}, args...)...)
- c.Stdin = os.Stdin
- c.Stdout = os.Stdout
- c.Stderr = os.Stderr
- return c.Run()
-}
func unit(ctx context.Context, action string, replace bool) error {
- if os.Geteuid() != 0 {
+ if !isRoot() {
if replace {
return elevated(ctx, action, "--replace")
}
return elevated(ctx, action)
}
- lock, e := service.Lock("/etc/fdpi/.operation.lock")
+ lock, e := service.Lock(service.OperationLockPath())
if e != nil {
return e
}
@@ -88,7 +73,7 @@ func unit(ctx context.Context, action string, replace bool) error {
}
func run(ctx context.Context, args []string) error {
if len(args) == 0 {
- if os.Geteuid() == 0 {
+ if tuiForbiddenForRoot() && isRoot() {
return fmt.Errorf("запускайте интерфейс без sudo; root нужен только для службы и административных действий")
}
st, e := os.Stdin.Stat()
@@ -109,7 +94,7 @@ func run(ctx context.Context, args []string) error {
}
switch args[0] {
case "tui":
- if os.Geteuid() == 0 {
+ if tuiForbiddenForRoot() && isRoot() {
return fmt.Errorf("открывайте интерфейс без sudo")
}
c, e := userConfig()
@@ -148,6 +133,8 @@ func run(ctx context.Context, args []string) error {
[--grep ТЕКСТ] [--category err|warning|info] — поиск и уровень записей
theme [НАЗВАНИЕ] Показать темы / выбрать тему
update Инструкция по безопасному обновлению
+ service-install Установить службу Windows и применить настройки (UAC)
+ service-remove Остановить и удалить службу Windows (UAC)
`)
return nil
@@ -169,13 +156,13 @@ func run(ctx context.Context, args []string) error {
}
return nil
case "daemon":
- if os.Geteuid() != 0 {
- return fmt.Errorf("для запуска фоновой службы нужны права root")
+ if !isRoot() {
+ return fmt.Errorf("для запуска фоновой службы нужны права администратора")
}
return service.Daemon()
case "cleanup":
- if os.Geteuid() != 0 {
- return fmt.Errorf("для удаления собственных сетевых правил нужны права root")
+ if !isRoot() {
+ return fmt.Errorf("для удаления собственных сетевых правил нужны права администратора")
}
return network.Cleanup(ctx)
case "apply":
@@ -183,8 +170,8 @@ func run(ctx context.Context, args []string) error {
case "rollback":
return elevated(ctx, "admin", "rollback")
case "admin":
- if os.Geteuid() != 0 {
- return fmt.Errorf("для административных действий нужны права root")
+ if !isRoot() {
+ return fmt.Errorf("для административных действий нужны права администратора")
}
if len(args) < 2 {
return fmt.Errorf("admin apply PATH | rollback")
@@ -196,7 +183,7 @@ func run(ctx context.Context, args []string) error {
}
return service.Apply(ctx, args[2])
case "rollback":
- return service.Apply(ctx, "/etc/fdpi/previous.json")
+ return service.Apply(ctx, service.PreviousConfig)
case "accounting-on":
if len(args) != 2 {
return fmt.Errorf("admin accounting-on не принимает дополнительных аргументов")
@@ -207,9 +194,22 @@ func run(ctx context.Context, args []string) error {
return fmt.Errorf("admin accounting-persist не принимает дополнительных аргументов")
}
return service.PersistAccounting()
+ case "windows-setup":
+ if len(args) != 2 {
+ return fmt.Errorf("admin windows-setup не принимает дополнительных аргументов")
+ }
+ return adminWindowsSetup(ctx)
default:
return fmt.Errorf("неизвестная административная команда")
}
+ case "windows-setup":
+ return elevated(ctx, "admin", "windows-setup")
+ case "service-install":
+ return elevated(ctx, "admin", "windows-setup")
+ case "service-remove":
+ return elevated(ctx, "windows-remove")
+ case "windows-remove":
+ return adminWindowsRemove(ctx)
case "profiles", "strategies":
c, e := userConfig()
if e != nil {
@@ -282,7 +282,7 @@ func run(ctx context.Context, args []string) error {
if len(args) < 2 || len(args) > 3 || (len(args) == 3 && args[2] != "--json") {
return fmt.Errorf("fdpi lab СЕРВИС [--json]")
}
- if os.Geteuid() != 0 {
+ if !isRoot() {
return elevated(ctx, args...)
}
r, e := diagnostics.Lab(ctx, args[1])
@@ -315,36 +315,7 @@ func run(ctx context.Context, args []string) error {
}
return nil
case "logs":
- a := []string{"-u", "fdpi.service", "--no-pager", "-n", "200"}
- for i := 1; i < len(args); i++ {
- switch args[i] {
- case "--follow":
- a = append(a, "--follow")
- case "--grep":
- i++
- if i >= len(args) {
- return fmt.Errorf("после --grep укажите текст поиска")
- }
- a = append(a, "--grep", args[i])
- case "--category":
- i++
- if i >= len(args) {
- return fmt.Errorf("--category err|warning|info")
- }
- switch args[i] {
- case "err", "warning", "info":
- a = append(a, "--priority", args[i])
- default:
- return fmt.Errorf("неверная категория журнала: используйте err, warning или info")
- }
- default:
- return fmt.Errorf("неизвестный параметр журнала")
- }
- }
- c := exec.CommandContext(ctx, "/usr/bin/journalctl", a...)
- c.Stdout = os.Stdout
- c.Stderr = os.Stderr
- return c.Run()
+ return runLogs(ctx, args[1:])
case "theme":
if len(args) == 1 {
for _, name := range tui.Themes {
@@ -363,7 +334,7 @@ func run(ctx context.Context, args []string) error {
case "setup":
return runSetup(ctx, args[1:])
case "update":
- fmt.Println("В каталоге исходников: make check && make build && sudo make install\nЕсли проект находится в git, сначала проверьте обновления и выполните git pull --ff-only.\nДвижок: make engine собирает закреплённую версию. Перед её сменой прочитайте docs/ENGINE.md.\nЗагрузка и запуск удалённого кода автоматически не выполняются.")
+ fmt.Println(updateHint())
return nil
}
return fmt.Errorf("неизвестная команда %q; справка: fdpi --help", args[0])
@@ -374,7 +345,7 @@ func configure(ctx context.Context, args []string) error {
return nil
}
if args[0] == "system" {
- if os.Geteuid() != 0 {
+ if !isRoot() {
return elevated(ctx, "config", "system")
}
c, e := config.Load(service.SystemConfig)
diff --git a/cmd/fdpi/onboarding_linux.go b/cmd/fdpi/onboarding_linux.go
new file mode 100644
index 0000000..bf33393
--- /dev/null
+++ b/cmd/fdpi/onboarding_linux.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+
+ "github.com/futureUnd1ground/fdpi/internal/config"
+)
+
+func onboardingPath() string {
+ return filepath.Join(filepath.Dir(config.UserPath()), "onboarding.json")
+}
+
+// needsOnboarding показывает мастер первого запуска, пока он не пройден
+// для текущей версии.
+func needsOnboarding() bool {
+ b, e := os.ReadFile(onboardingPath())
+ if e == nil {
+ var saved struct {
+ Version string `json:"version"`
+ }
+ if json.Unmarshal(b, &saved) == nil && saved.Version == version {
+ return false
+ }
+ }
+ return true
+}
diff --git a/cmd/fdpi/onboarding_windows.go b/cmd/fdpi/onboarding_windows.go
new file mode 100644
index 0000000..6804d86
--- /dev/null
+++ b/cmd/fdpi/onboarding_windows.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+ "context"
+ "errors"
+ "os"
+ "time"
+
+ "github.com/futureUnd1ground/fdpi/internal/service"
+)
+
+// needsOnboarding показывает мастер первого запуска, пока служба не установлена
+// или системные настройки отсутствуют.
+func needsOnboarding() bool {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ st := service.State(ctx)
+ _, e := os.Stat(service.SystemConfig)
+ return !st.Installed || errors.Is(e, os.ErrNotExist)
+}
diff --git a/cmd/fdpi/platform_linux.go b/cmd/fdpi/platform_linux.go
new file mode 100644
index 0000000..e08755a
--- /dev/null
+++ b/cmd/fdpi/platform_linux.go
@@ -0,0 +1,43 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+)
+
+// isRoot сообщает, запущен ли процесс от root.
+func isRoot() bool { return os.Geteuid() == 0 }
+
+// tuiForbiddenForRoot запрещает интерфейс от root (на Linux sudo не нужен).
+func tuiForbiddenForRoot() bool { return true }
+
+// elevated выполняет команду с правами root через sudo.
+func elevated(ctx context.Context, args ...string) error {
+ exe, e := os.Executable()
+ if e != nil {
+ return e
+ }
+ if isRoot() {
+ return run(ctx, args)
+ }
+ c := exec.CommandContext(ctx, "sudo", append([]string{"--", exe}, args...)...)
+ c.Stdin = os.Stdin
+ c.Stdout = os.Stdout
+ c.Stderr = os.Stderr
+ return c.Run()
+}
+
+// updateHint — инструкция по обновлению из исходников.
+func updateHint() string {
+ return "В каталоге исходников: make check && make build && sudo make install\nЕсли проект находится в git, сначала проверьте обновления и выполните git pull --ff-only.\nДвижок: make engine собирает закреплённую версию. Перед её сменой прочитайте docs/ENGINE.md.\nЗагрузка и запуск удалённого кода автоматически не выполняются."
+}
+
+func adminWindowsSetup(ctx context.Context) error {
+ return fmt.Errorf("команда доступна только на Windows")
+}
+
+func adminWindowsRemove(ctx context.Context) error {
+ return fmt.Errorf("команда доступна только на Windows")
+}
diff --git a/cmd/fdpi/platform_windows.go b/cmd/fdpi/platform_windows.go
new file mode 100644
index 0000000..4152c9f
--- /dev/null
+++ b/cmd/fdpi/platform_windows.go
@@ -0,0 +1,142 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "unsafe"
+
+ "github.com/futureUnd1ground/fdpi/internal/config"
+ "github.com/futureUnd1ground/fdpi/internal/service"
+ "golang.org/x/sys/windows"
+)
+
+// isRoot сообщает, обладает ли процесс повышенными правами (UAC).
+func isRoot() bool { return service.IsAdmin() }
+
+// tuiForbiddenForRoot: на Windows интерфейс работает и от администратора.
+func tuiForbiddenForRoot() bool { return false }
+
+// shellExecuteInfo — SHELLEXECUTEINFOW для ShellExecuteExW.
+type shellExecuteInfo struct {
+ cbSize uint32
+ fMask uint32
+ hwnd windows.Handle
+ lpVerb *uint16
+ lpFile *uint16
+ lpParameters *uint16
+ lpDirectory *uint16
+ nShow int32
+ hInstApp windows.Handle
+ lpIDList uintptr
+ lpClass *uint16
+ hkeyClass windows.Handle
+ dwHotKey uint32
+ hMonitor windows.Handle
+ hProcess windows.Handle
+}
+
+// elevated выполняет команду с повышением прав через UAC (ShellExecuteEx "runas").
+func elevated(ctx context.Context, args ...string) error {
+ if isRoot() {
+ return run(ctx, args)
+ }
+ exe, e := os.Executable()
+ if e != nil {
+ return e
+ }
+ exe, e = filepath.Abs(exe)
+ if e != nil {
+ return e
+ }
+ verb, e := windows.UTF16PtrFromString("runas")
+ if e != nil {
+ return e
+ }
+ file, e := windows.UTF16PtrFromString(exe)
+ if e != nil {
+ return e
+ }
+ params := ""
+ if len(args) > 0 {
+ quoted := make([]string, len(args))
+ for i, a := range args {
+ quoted[i] = `"` + strings.ReplaceAll(a, `"`, `\"`) + `"`
+ }
+ params = strings.Join(quoted, " ")
+ }
+ paramsPtr, e := windows.UTF16PtrFromString(params)
+ if e != nil {
+ return e
+ }
+ dirPtr, e := windows.UTF16PtrFromString(filepath.Dir(exe))
+ if e != nil {
+ return e
+ }
+ const (
+ seeMaskNoCloseProcess = 0x40 // SEE_MASK_NOCLOSEPROCESS — получить hProcess для ожидания
+ seeMaskFlagNoUI = 0x400 // SEE_MASK_FLAG_NO_UI — ошибки без диалогов
+ swHide = 0
+ )
+ sei := &shellExecuteInfo{
+ fMask: seeMaskNoCloseProcess | seeMaskFlagNoUI,
+ lpVerb: verb,
+ lpFile: file,
+ lpParameters: paramsPtr,
+ lpDirectory: dirPtr,
+ nShow: swHide,
+ }
+ sei.cbSize = uint32(unsafe.Sizeof(*sei))
+ shellExecuteExW := windows.NewLazySystemDLL("shell32.dll").NewProc("ShellExecuteExW")
+ r, _, callErr := shellExecuteExW.Call(uintptr(unsafe.Pointer(sei)))
+ if r == 0 {
+ if callErr == windows.ERROR_CANCELLED {
+ return fmt.Errorf("действие отменено: повышение прав не подтверждено")
+ }
+ if callErr != windows.ERROR_SUCCESS {
+ return fmt.Errorf("ShellExecuteEx: %v", callErr)
+ }
+ return fmt.Errorf("ShellExecuteEx не удался (код %d)", sei.hInstApp)
+ }
+ if sei.hProcess == 0 {
+ return nil
+ }
+ defer windows.CloseHandle(sei.hProcess)
+ windows.WaitForSingleObject(sei.hProcess, windows.INFINITE)
+ var code uint32
+ if e := windows.GetExitCodeProcess(sei.hProcess, &code); e == nil && code != 0 {
+ return fmt.Errorf("команда завершилась с кодом %d", code)
+ }
+ return nil
+}
+
+// updateHint — инструкция по обновлению на Windows.
+func updateHint() string {
+ return "Скачайте новую версию FDPI для Windows и распакуйте поверх каталога программы,\nсохранив рядом с fdpi.exe файлы winws2.exe, WinDivert.dll, WinDivert64.sys и каталог lua.\nЗатем перезапустите службу: fdpi restart.\nЗагрузка и запуск удалённого кода автоматически не выполняются."
+}
+
+// adminWindowsSetup устанавливает службу и применяет текущий черновик настроек.
+// Вызывается только из процесса с правами администратора (через elevated).
+func adminWindowsSetup(ctx context.Context) error {
+ if e := service.EnsureStateDir(); e != nil {
+ return e
+ }
+ if e := service.Install(ctx); e != nil {
+ return e
+ }
+ if e := service.Apply(ctx, config.UserPath()); e != nil {
+ return e
+ }
+ return service.Control(ctx, "start", false)
+}
+
+// adminWindowsRemove останавливает и удаляет службу FDPI.
+func adminWindowsRemove(ctx context.Context) error {
+ if !service.IsAdmin() {
+ return fmt.Errorf("для удаления службы нужны права администратора")
+ }
+ _ = service.Control(ctx, "stop", false)
+ return service.Remove(ctx)
+}
diff --git a/cmd/fdpi/setup.go b/cmd/fdpi/setup_linux.go
similarity index 100%
rename from cmd/fdpi/setup.go
rename to cmd/fdpi/setup_linux.go
diff --git a/cmd/fdpi/setup_output.go b/cmd/fdpi/setup_output_linux.go
similarity index 100%
rename from cmd/fdpi/setup_output.go
rename to cmd/fdpi/setup_output_linux.go
diff --git a/cmd/fdpi/setup_tui.go b/cmd/fdpi/setup_tui_linux.go
similarity index 97%
rename from cmd/fdpi/setup_tui.go
rename to cmd/fdpi/setup_tui_linux.go
index 3b42112..3406041 100644
--- a/cmd/fdpi/setup_tui.go
+++ b/cmd/fdpi/setup_tui_linux.go
@@ -396,21 +396,6 @@ func (m setupModel) View() string {
}
return tui.Frame(m.width, m.height, tui.Theme("FDPI"), []string{"① Начало", "② Параметры", "③ Установка", "④ Готово"}, m.page, "Установка и настройка", "МАСТЕР", strings.Join(lines, "\n"), "[↑↓] Выбор [Space] Флажок [Enter] Продолжить [Q] Выход")
}
-func onboardingPath() string {
- return filepath.Join(filepath.Dir(config.UserPath()), "onboarding.json")
-}
-func needsOnboarding() bool {
- b, e := os.ReadFile(onboardingPath())
- if e == nil {
- var saved struct {
- Version string `json:"version"`
- }
- if json.Unmarshal(b, &saved) == nil && saved.Version == version {
- return false
- }
- }
- return true
-}
func runSetupTUI(ctx context.Context, source string) (bool, error) {
result, e := tea.NewProgram(newSetupModel(ctx, source), tea.WithAltScreen(), tea.WithContext(ctx)).Run()
if e != nil {
diff --git a/cmd/fdpi/setup_tui_test.go b/cmd/fdpi/setup_tui_linux_test.go
similarity index 100%
rename from cmd/fdpi/setup_tui_test.go
rename to cmd/fdpi/setup_tui_linux_test.go
diff --git a/cmd/fdpi/setup_tui_windows.go b/cmd/fdpi/setup_tui_windows.go
new file mode 100644
index 0000000..52ac510
--- /dev/null
+++ b/cmd/fdpi/setup_tui_windows.go
@@ -0,0 +1,339 @@
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/x/ansi"
+
+ "github.com/futureUnd1ground/fdpi/internal/config"
+ "github.com/futureUnd1ground/fdpi/internal/diagnostics"
+ "github.com/futureUnd1ground/fdpi/internal/service"
+ "github.com/futureUnd1ground/fdpi/internal/tui"
+)
+
+var errSetupCancelled = errors.New("мастер остановлен; уже завершённые шаги сохранены")
+
+type winInstallStep struct {
+ title, description string
+ action func() (string, error)
+}
+
+type winSetupModel struct {
+ ctx context.Context
+ cfg config.Config
+ components []componentCheck
+ page, cursor int
+ width, height int
+ strategy int // курсор в списке стратегий
+ steps []winInstallStep
+ current int
+ completed []string
+ err, detail, probes string
+ loaded, busy bool
+ openApp bool
+}
+
+func newWinSetupModel(ctx context.Context) winSetupModel {
+ return winSetupModel{ctx: ctx, width: 80, height: 26}
+}
+
+// optionCount — число позиций на странице выбора сервисов и стратегии.
+func (m winSetupModel) optionCount() int {
+ return len(m.cfg.Profiles) + len(m.cfg.Strategies) + 2
+}
+
+// plan собирает шаги установки.
+func (m *winSetupModel) plan() error {
+ enabled := false
+ for _, p := range m.cfg.Profiles {
+ if p.Enabled {
+ enabled = true
+ }
+ }
+ if !enabled {
+ return fmt.Errorf("отметьте хотя бы один сервис")
+ }
+ strategy := m.cfg.Strategies[m.strategy].ID
+ cfg := m.cfg
+ m.steps = []winInstallStep{
+ {title: "Сохранить настройки", description: "Записать выбранные сервисы и стратегию в пользовательский черновик.", action: func() (string, error) {
+ applyDefaultStrategy(&cfg, strategy)
+ if e := cfg.Validate(); e != nil {
+ return "", e
+ }
+ m.cfg = cfg
+ return "Настройки сохранены", config.Save(config.UserPath(), cfg)
+ }},
+ {title: "Установить и запустить службу", description: "Регистрация службы fdpi с автозапуском, применение конфигурации в ProgramData\\FDPI и запуск обработки. Один запрос UAC.", action: func() (string, error) {
+ if e := service.EnsureStateDir(); e != nil {
+ return "", e
+ }
+ return "Служба установлена и запущена", elevated(m.ctx, "windows-setup")
+ }},
+ }
+ m.current = 0
+ m.completed = nil
+ return nil
+}
+
+// probeEnabled проверяет первый домен каждого включённого профиля.
+func (m *winSetupModel) probeEnabled() (string, error) {
+ var b strings.Builder
+ ok := true
+ for _, p := range m.cfg.Profiles {
+ if !p.Enabled || len(p.Domains) == 0 {
+ continue
+ }
+ r := diagnostics.Probe(m.ctx, p.Domains[0])
+ fmt.Fprintf(&b, "[%s] %s (%s): ", map[bool]string{true: "OK", false: "ОШИБКА"}[r.Reachable], p.Title(), p.Domains[0])
+ if r.Reachable {
+ fmt.Fprintf(&b, "HTTP %d, %d мс\n", r.HTTP, r.Millis)
+ } else {
+ fmt.Fprintf(&b, "%s (%s)\n", r.Error, r.Stage)
+ }
+ ok = ok && r.Reachable
+ }
+ if b.Len() == 0 {
+ return "Нет включённых профилей с доменами.", nil
+ }
+ if !ok {
+ return b.String(), fmt.Errorf("часть проверок не пройдена; журнал: fdpi logs")
+ }
+ return b.String(), nil
+}
+
+type winSetupLoaded struct {
+ cfg config.Config
+ components []componentCheck
+ err error
+}
+type winStepDone struct {
+ text string
+ err error
+}
+
+func (m winSetupModel) Init() tea.Cmd {
+ return func() tea.Msg {
+ c, e := userConfig()
+ return winSetupLoaded{cfg: c, components: checkComponents(), err: e}
+ }
+}
+func (m winSetupModel) executeCurrent() tea.Cmd {
+ s := m.steps[m.current]
+ return func() tea.Msg {
+ text, e := s.action()
+ return winStepDone{text: text, err: e}
+ }
+}
+func (m winSetupModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch x := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.width = x.Width
+ m.height = x.Height
+ case winSetupLoaded:
+ m.loaded = true
+ m.cfg = x.cfg
+ m.components = x.components
+ if x.err != nil {
+ m.err = x.err.Error()
+ }
+ case winStepDone:
+ m.busy = false
+ m.detail = x.text
+ if x.err != nil {
+ m.err = x.err.Error()
+ return m, nil
+ }
+ m.completed = append(m.completed, m.steps[m.current].title)
+ m.current++
+ m.err = ""
+ if m.current == len(m.steps) {
+ text, e := m.probeEnabled()
+ m.probes = text
+ m.page = 3
+ if e != nil {
+ m.err = e.Error()
+ }
+ }
+ case tea.KeyMsg:
+ k := x.String()
+ if k == "ctrl+c" || k == "q" {
+ return m, tea.Quit
+ }
+ if m.busy {
+ return m, nil
+ }
+ switch m.page {
+ case 0:
+ switch k {
+ case "up", "k":
+ m.cursor = max(0, m.cursor-1)
+ case "down", "j":
+ m.cursor = min(1, m.cursor+1)
+ case "enter":
+ if m.cursor == 1 {
+ return m, tea.Quit
+ }
+ if !m.loaded {
+ m.err = "Проверка компонентов ещё выполняется"
+ return m, nil
+ }
+ if missing := missingComponents(m.components); len(missing) > 0 {
+ m.err = "Не хватает файлов: " + strings.Join(missing, ", ") + ". Распакуйте полный комплект FDPI в один каталог."
+ return m, nil
+ }
+ m.page = 1
+ m.cursor = 0
+ m.err = ""
+ }
+ case 1:
+ count := m.optionCount()
+ switch k {
+ case "up", "k":
+ m.cursor = max(0, m.cursor-1)
+ case "down", "j":
+ m.cursor = min(count-1, m.cursor+1)
+ case "esc":
+ m.page = 0
+ m.cursor = 0
+ case " ", "enter":
+ n := len(m.cfg.Profiles)
+ s := len(m.cfg.Strategies)
+ switch {
+ case m.cursor < n:
+ m.cfg.Profiles[m.cursor].Enabled = !m.cfg.Profiles[m.cursor].Enabled
+ case m.cursor < n+s:
+ if k == "enter" {
+ m.strategy = m.cursor - n
+ }
+ case m.cursor == n+s: // Установить и запустить
+ if k != "enter" {
+ break
+ }
+ if e := m.plan(); e != nil {
+ m.err = e.Error()
+ } else {
+ m.page = 2
+ m.err = ""
+ }
+ default: // Назад
+ m.page = 0
+ m.cursor = 0
+ }
+ }
+ case 2:
+ if k == "enter" {
+ m.busy = true
+ m.err = ""
+ return m, m.executeCurrent()
+ }
+ case 3:
+ if k == "enter" {
+ m.openApp = true
+ return m, tea.Quit
+ }
+ }
+ }
+ return m, nil
+}
+
+func (m winSetupModel) options() []string {
+ var out []string
+ box := func(b bool) string {
+ if b {
+ return "[✓] "
+ }
+ return "[ ] "
+ }
+ for _, p := range m.cfg.Profiles {
+ out = append(out, box(p.Enabled)+p.Title())
+ }
+ for i, s := range m.cfg.Strategies {
+ mark := "( ) "
+ if i == m.strategy {
+ mark = "(●) "
+ }
+ out = append(out, mark+"стратегия по умолчанию: "+s.Name)
+ }
+ out = append(out, "[ Установить и запустить ]", "[ Назад ]")
+ return out
+}
+
+func (m winSetupModel) View() string {
+ w := max(20, m.width-6)
+ var b strings.Builder
+ b.WriteString("Мастер настраивает службу Windows; установка отдельных пакетов не требуется.\n\n")
+ switch m.page {
+ case 0:
+ b.WriteString("Компоненты рядом с fdpi.exe:\n")
+ for _, c := range m.components {
+ mark := "[✓] "
+ if !c.ok {
+ mark = "[✗] "
+ }
+ b.WriteString(mark + c.name + "\n")
+ }
+ if !m.loaded {
+ b.WriteString("Проверка…\n")
+ }
+ b.WriteString("\n")
+ for i, label := range []string{"Продолжить", "Выйти"} {
+ prefix := " "
+ if m.cursor == i {
+ prefix = "▸ "
+ }
+ b.WriteString(prefix + label + "\n")
+ }
+ case 1:
+ b.WriteString("Пробел — включить сервис; Enter — выбрать стратегию по умолчанию.\nВыбранная стратегия назначается всем включённым сервисам.\n\n")
+ options := m.options()
+ start := max(0, m.cursor-max(3, m.height-13)+1)
+ for i := start; i < len(options) && i < start+max(3, m.height-13); i++ {
+ prefix := " "
+ if m.cursor == i {
+ prefix = "▸ "
+ }
+ b.WriteString(prefix + options[i] + "\n")
+ }
+ case 2:
+ s := m.steps[m.current]
+ fmt.Fprintf(&b, "Шаг %d / %d · %s\n\n%s\n\n", m.current+1, len(m.steps), s.title, s.description)
+ if m.busy {
+ b.WriteString("Выполняется…\n")
+ } else {
+ b.WriteString("Enter — выполнить / повторить этот шаг.\n")
+ }
+ for _, done := range m.completed {
+ b.WriteString("✓ " + done + "\n")
+ }
+ if m.detail != "" {
+ b.WriteString("\n" + m.detail)
+ }
+ case 3:
+ b.WriteString("Проверка доступа к включённым сервисам:\n\n" + m.probes + "\n")
+ b.WriteString("Проверка не подтверждает видео, звонки и эффективность обхода DPI.\n")
+ b.WriteString("\nEnter — завершить установку")
+ }
+ if m.err != "" {
+ b.WriteString("\nОшибка: " + m.err + "\n")
+ }
+ lines := strings.Split(ansi.Wrap(b.String(), w, ""), "\n")
+ if len(lines) > max(5, m.height-2) {
+ lines = lines[:max(5, m.height-3)]
+ lines = append(lines, ansi.Truncate(m.err, w, "…"))
+ }
+ return tui.Frame(m.width, m.height, tui.Theme("FDPI"), []string{"① Компоненты", "② Сервисы", "③ Установка", "④ Готово"}, min(m.page, 3), "Установка и настройка", "МАСТЕР", strings.Join(lines, "\n"), "[↑↓] Выбор [Space] Флажок [Enter] Продолжить [Q] Выход")
+}
+
+func runWinSetupTUI(ctx context.Context, m winSetupModel) (bool, error) {
+ result, e := tea.NewProgram(m, tea.WithAltScreen(), tea.WithContext(ctx)).Run()
+ if e != nil {
+ return false, e
+ }
+ fin := result.(winSetupModel)
+ return fin.openApp, nil
+}
diff --git a/cmd/fdpi/setup_windows.go b/cmd/fdpi/setup_windows.go
new file mode 100644
index 0000000..1603815
--- /dev/null
+++ b/cmd/fdpi/setup_windows.go
@@ -0,0 +1,101 @@
+// Windows: мастер первого запуска настраивает службу FDPI через SCM —
+// сборка из исходников и пакетный менеджер не требуются.
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/futureUnd1ground/fdpi/internal/config"
+ "github.com/futureUnd1ground/fdpi/internal/service"
+ "github.com/futureUnd1ground/fdpi/internal/tui"
+)
+
+// componentCheck — проверка наличия файла комплекта рядом с fdpi.exe.
+type componentCheck struct {
+ name string
+ path string
+ ok bool
+}
+
+// checkComponents проверяет движок winws2, драйвер WinDivert и Lua-библиотеки.
+func checkComponents() []componentCheck {
+ exe, e := os.Executable()
+ if e != nil {
+ exe = "fdpi.exe"
+ }
+ dir := filepath.Dir(exe)
+ luaDir := filepath.Join(dir, "lua")
+ checks := []componentCheck{
+ {"Движок winws2.exe", filepath.Join(dir, "winws2.exe"), false},
+ {"WinDivert.dll", filepath.Join(dir, "WinDivert.dll"), false},
+ {"Драйвер WinDivert64.sys", filepath.Join(dir, "WinDivert64.sys"), false},
+ {"lua\\zapret-lib.lua", filepath.Join(luaDir, "zapret-lib.lua"), false},
+ {"lua\\zapret-antidpi.lua", filepath.Join(luaDir, "zapret-antidpi.lua"), false},
+ }
+ for i := range checks {
+ st, e := os.Stat(checks[i].path)
+ checks[i].ok = e == nil && !st.IsDir()
+ }
+ return checks
+}
+
+func missingComponents(checks []componentCheck) []string {
+ var out []string
+ for _, c := range checks {
+ if !c.ok {
+ out = append(out, c.name)
+ }
+ }
+ return out
+}
+
+// applyDefaultStrategy назначает выбранную стратегию всем включённым профилям.
+func applyDefaultStrategy(c *config.Config, strategy string) {
+ for i := range c.Profiles {
+ if c.Profiles[i].Enabled {
+ c.Profiles[i].Strategy = strategy
+ }
+ }
+}
+
+func runSetup(ctx context.Context, args []string) error {
+ if len(args) > 0 {
+ return fmt.Errorf("на Windows мастер не принимает параметров; справка: fdpi --help")
+ }
+ if e := os.MkdirAll(filepath.Dir(config.UserPath()), 0700); e != nil {
+ return e
+ }
+ lock, e := service.Lock(filepath.Join(filepath.Dir(config.UserPath()), "setup.lock"))
+ if e != nil {
+ return fmt.Errorf("мастер уже открыт: закройте прежнее окно установки и снова выполните fdpi: %w", e)
+ }
+ unlock := func() {
+ if lock != nil {
+ service.Unlock(lock)
+ lock = nil
+ }
+ }
+ defer unlock()
+ m := newWinSetupModel(ctx)
+ result, e := runWinSetupTUI(ctx, m)
+ unlock()
+ if e != nil {
+ if errors.Is(e, errSetupCancelled) {
+ fmt.Println("\nМастер остановлен. Уже выполненные действия сохранены. Продолжить: fdpi setup.")
+ return nil
+ }
+ return e
+ }
+ if result {
+ c, e := userConfig()
+ if e != nil {
+ return e
+ }
+ return tui.Run(c, config.UserPath())
+ }
+ return nil
+}
diff --git a/cmd/fdpi/wizard.go b/cmd/fdpi/wizard_linux.go
similarity index 100%
rename from cmd/fdpi/wizard.go
rename to cmd/fdpi/wizard_linux.go
diff --git a/cmd/fdpi/wizard_test.go b/cmd/fdpi/wizard_linux_test.go
similarity index 100%
rename from cmd/fdpi/wizard_test.go
rename to cmd/fdpi/wizard_linux_test.go
diff --git a/docs/FILES.md b/docs/FILES.md
index 4a1b15f..005ad65 100644
--- a/docs/FILES.md
+++ b/docs/FILES.md
@@ -28,7 +28,6 @@
- [docs/SPOTIFY_RU.md](../docs/SPOTIFY_RU.md)
- [docs/THEMES.md](../docs/THEMES.md)
- [docs/VALIDATION.md](../docs/VALIDATION.md)
-- [docs/assets/banner.png](../docs/assets/banner.png)
- [docs/assets/dashboard.png](../docs/assets/dashboard.png)
- [docs/assets/installation.png](../docs/assets/installation.png)
- [docs/assets/logs.png](../docs/assets/logs.png)
diff --git a/docs/INSTALL_RU.md b/docs/INSTALL_RU.md
index cb81e26..a92c647 100644
--- a/docs/INSTALL_RU.md
+++ b/docs/INSTALL_RU.md
@@ -6,6 +6,9 @@
Для проверки главной: `fdpi test-service youtube`. HTTP 200 страницы
не означает, что API и видео доступны.
+Windows-версия ставится из готового архива без сборки:
+[README_WINDOWS.md](../README_WINDOWS.md).
+
## Первый запуск: отдельный TUI установщика
diff --git a/docs/assets/banner.png b/docs/assets/banner.png
deleted file mode 100644
index 5353242..0000000
Binary files a/docs/assets/banner.png and /dev/null differ
diff --git a/internal/diagnostics/bind_linux.go b/internal/diagnostics/bind_linux.go
new file mode 100644
index 0000000..bb6a7a1
--- /dev/null
+++ b/internal/diagnostics/bind_linux.go
@@ -0,0 +1,23 @@
+package diagnostics
+
+import (
+ "net"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+)
+
+// dialControl возвращает Control-функцию Dialer, привязывающую сокеты к интерфейсу.
+func dialControl(iface string) (func(string, string, syscall.RawConn) error, error) {
+ if _, err := net.InterfaceByName(iface); err != nil {
+ return nil, err
+ }
+ return func(_, _ string, c syscall.RawConn) error {
+ var err error
+ e := c.Control(func(fd uintptr) { err = unix.SetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_BINDTODEVICE, iface) })
+ if e != nil {
+ return e
+ }
+ return err
+ }, nil
+}
diff --git a/internal/diagnostics/bind_windows.go b/internal/diagnostics/bind_windows.go
new file mode 100644
index 0000000..9d486e7
--- /dev/null
+++ b/internal/diagnostics/bind_windows.go
@@ -0,0 +1,32 @@
+package diagnostics
+
+import (
+ "net"
+ "syscall"
+
+ "golang.org/x/sys/windows"
+)
+
+// ipUnicastIf — IP_UNICAST_IF из ws2ipdef.h (в x/sys/windows нет готовой константы).
+const ipUnicastIf = 31
+
+// dialControl возвращает Control-функцию Dialer, привязывающую исходящие
+// IPv4-сокеты к интерфейсу (IP_UNICAST_IF). Для IPv6-сокетов эта опция
+// может не применяться — ошибки игнорируются, чтобы не ломать дуалы.
+func dialControl(iface string) (func(string, string, syscall.RawConn) error, error) {
+ ifi, err := net.InterfaceByName(iface)
+ if err != nil {
+ return nil, err
+ }
+ index := ifi.Index
+ return func(_, _ string, c syscall.RawConn) error {
+ var err error
+ e := c.Control(func(fd uintptr) {
+ err = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, ipUnicastIf, index)
+ })
+ if e != nil {
+ return e
+ }
+ return err
+ }, nil
+}
diff --git a/internal/diagnostics/diagnostics.go b/internal/diagnostics/diagnostics_common.go
similarity index 66%
rename from internal/diagnostics/diagnostics.go
rename to internal/diagnostics/diagnostics_common.go
index 1732203..dd6a2f5 100644
--- a/internal/diagnostics/diagnostics.go
+++ b/internal/diagnostics/diagnostics_common.go
@@ -7,14 +7,10 @@ import (
"fmt"
"github.com/futureUnd1ground/fdpi/internal/config"
"github.com/futureUnd1ground/fdpi/internal/engine"
- "github.com/futureUnd1ground/fdpi/internal/network"
"github.com/futureUnd1ground/fdpi/internal/service"
"io"
"net"
"net/http"
- "os"
- "os/exec"
- "strings"
"time"
)
@@ -112,72 +108,6 @@ type Check struct {
Detail string `json:"detail"`
}
-func Doctor(ctx context.Context) []Check {
- var out []Check
- add := func(n string, e error, d string) {
- if e != nil {
- d = e.Error()
- }
- out = append(out, Check{n, e == nil, d})
- }
- _, e := os.Stat(engine.Path)
- add("Файл движка", e, engine.Path)
- c, e := config.Load(service.SystemConfig)
- add("Системные настройки", e, service.SystemConfig)
- if e == nil {
- dir, e := os.MkdirTemp("", "fdpi-doctor-")
- if e == nil {
- defer os.RemoveAll(dir)
- a, _, _, ae := engine.Args(c, dir)
- if ae == nil {
- ae = engine.Check(ctx, engine.Path, a)
- }
- add("Совместимость движка", ae, "параметры приняты реальным движком через --dry-run")
- }
- }
- b, e := exec.CommandContext(ctx, "/usr/bin/nft", "--version").CombinedOutput()
- add("nftables", e, strings.TrimSpace(string(b)))
- _, e = network.Owned(ctx)
- add("Доступ к таблице и её принадлежность", e, "проверка таблицы без изменений выполнена")
- b, e = exec.CommandContext(ctx, "/usr/bin/nft", "-j", "list", "ruleset").CombinedOutput()
- if e == nil {
- text := string(b)
- if strings.Contains(text, "flowtable") || strings.Contains(text, "\"queue\"") {
- out = append(out, Check{"Возможные конфликты firewall", false, "найдена очередь или flowtable; проверьте nft list ruleset, включая правила самого FDPI"})
- } else {
- add("Возможные конфликты firewall", nil, "очереди и flowtable не найдены; другие взаимодействия с firewall всё же возможны")
- }
- }
- acct, ae := os.ReadFile("/proc/sys/net/netfilter/nf_conntrack_acct")
- if ae == nil && strings.TrimSpace(string(acct)) != "1" {
- ae = fmt.Errorf("нужно значение net.netfilter.nf_conntrack_acct=1; FDPI не меняет его автоматически")
- }
- add("Учёт соединений conntrack", ae, strings.TrimSpace(string(acct)))
- _, e = os.Stat("/proc/net/netfilter/nfnetlink_queue")
- add("Поддержка NFQUEUE в ядре", e, "доступна; может потребоваться загрузка модуля")
- b, e = os.ReadFile("/proc/self/status")
- add("Полномочия процесса в ядре", e, capLine(string(b)))
- add("Права администратора", func() error {
- if os.Geteuid() != 0 {
- return fmt.Errorf("обычный пользователь — это нормально для интерфейса; административные действия используют sudo")
- }
- return nil
- }(), "root")
- _, e = net.DefaultResolver.LookupHost(ctx, "example.com")
- add("DNS", e, "адрес example.com получен")
- s := service.State(ctx)
- out = append(out, Check{"Служба systemd", s.Detail == "" && s.Installed, s.StateText()})
- return out
-}
-func capLine(s string) string {
- for _, l := range strings.Split(s, "\n") {
- if strings.HasPrefix(l, "CapEff:") {
- return l
- }
- }
- return "поле CapEff недоступно"
-}
-
type Trial struct {
Strategy string `json:"strategy"`
YouTube *YouTubeReport `json:"youtube,omitempty"`
@@ -195,8 +125,8 @@ type Report struct {
func Lab(ctx context.Context, id string) (report Report, err error) {
report.Profile = id
report.Note = "Проверяется доступность HTTPS. HTTP 403/451 может означать блокировку приложения. QUIC и медиатрафик не проверяются. Рекомендация появляется только при улучшении относительно неудачной базовой проверки."
- if os.Geteuid() != 0 {
- return report, fmt.Errorf("для подбора нужны права root и установленная служба systemd")
+ if e := requireRoot(); e != nil {
+ return report, e
}
if err = engine.Trusted(service.SystemConfig); err != nil {
return
@@ -204,7 +134,7 @@ func Lab(ctx context.Context, id string) (report Report, err error) {
if err = engine.Trusted(engine.Path); err != nil {
return
}
- lock, e := service.Lock("/etc/fdpi/.operation.lock")
+ lock, e := service.Lock(service.OperationLockPath())
if e != nil {
return report, e
}
@@ -243,7 +173,7 @@ func Lab(ctx context.Context, id string) (report Report, err error) {
if wasActive {
action = "restart"
}
- _, se := service.Command(restore, action, "fdpi.service")
+ se := restoreService(restore, action)
if e != nil || se != nil {
err = fmt.Errorf("результат подбора: %v; ОШИБКА ВОССТАНОВЛЕНИЯ: настройки=%v служба=%v", err, e, se)
}
diff --git a/internal/diagnostics/doctor_linux.go b/internal/diagnostics/doctor_linux.go
new file mode 100644
index 0000000..9c4084d
--- /dev/null
+++ b/internal/diagnostics/doctor_linux.go
@@ -0,0 +1,95 @@
+package diagnostics
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/futureUnd1ground/fdpi/internal/config"
+ "github.com/futureUnd1ground/fdpi/internal/engine"
+ "github.com/futureUnd1ground/fdpi/internal/network"
+ "github.com/futureUnd1ground/fdpi/internal/service"
+)
+
+func Doctor(ctx context.Context) []Check {
+ var out []Check
+ add := func(n string, e error, d string) {
+ if e != nil {
+ d = e.Error()
+ }
+ out = append(out, Check{n, e == nil, d})
+ }
+ _, e := os.Stat(engine.Path)
+ add("Файл движка", e, engine.Path)
+ c, e := config.Load(service.SystemConfig)
+ add("Системные настройки", e, service.SystemConfig)
+ if e == nil {
+ dir, e := os.MkdirTemp("", "fdpi-doctor-")
+ if e == nil {
+ defer os.RemoveAll(dir)
+ a, _, _, ae := engine.Args(c, dir)
+ if ae == nil {
+ ae = engine.Check(ctx, engine.Path, a)
+ }
+ add("Совместимость движка", ae, "параметры приняты реальным движком через --dry-run")
+ }
+ }
+ b, e := exec.CommandContext(ctx, "/usr/bin/nft", "--version").CombinedOutput()
+ add("nftables", e, strings.TrimSpace(string(b)))
+ _, e = network.Owned(ctx)
+ add("Доступ к таблице и её принадлежность", e, "проверка таблицы без изменений выполнена")
+ b, e = exec.CommandContext(ctx, "/usr/bin/nft", "-j", "list", "ruleset").CombinedOutput()
+ if e == nil {
+ text := string(b)
+ if strings.Contains(text, "flowtable") || strings.Contains(text, "\"queue\"") {
+ out = append(out, Check{"Возможные конфликты firewall", false, "найдена очередь или flowtable; проверьте nft list ruleset, включая правила самого FDPI"})
+ } else {
+ add("Возможные конфликты firewall", nil, "очереди и flowtable не найдены; другие взаимодействия с firewall всё же возможны")
+ }
+ }
+ acct, ae := os.ReadFile("/proc/sys/net/netfilter/nf_conntrack_acct")
+ if ae == nil && strings.TrimSpace(string(acct)) != "1" {
+ ae = fmt.Errorf("нужно значение net.netfilter.nf_conntrack_acct=1; FDPI не меняет его автоматически")
+ }
+ add("Учёт соединений conntrack", ae, strings.TrimSpace(string(acct)))
+ _, e = os.Stat("/proc/net/netfilter/nfnetlink_queue")
+ add("Поддержка NFQUEUE в ядре", e, "доступна; может потребоваться загрузка модуля")
+ b, e = os.ReadFile("/proc/self/status")
+ add("Полномочия процесса в ядре", e, capLine(string(b)))
+ add("Права администратора", func() error {
+ if os.Geteuid() != 0 {
+ return fmt.Errorf("обычный пользователь — это нормально для интерфейса; административные действия используют sudo")
+ }
+ return nil
+ }(), "root")
+ _, e = net.DefaultResolver.LookupHost(ctx, "example.com")
+ add("DNS", e, "адрес example.com получен")
+ s := service.State(ctx)
+ out = append(out, Check{"Служба systemd", s.Detail == "" && s.Installed, s.StateText()})
+ return out
+}
+func capLine(s string) string {
+ for _, l := range strings.Split(s, "\n") {
+ if strings.HasPrefix(l, "CapEff:") {
+ return l
+ }
+ }
+ return "поле CapEff недоступно"
+}
+
+// requireRoot проверяет полномочия для подбора стратегий (lab).
+func requireRoot() error {
+ if os.Geteuid() != 0 {
+ return fmt.Errorf("для подбора нужны права root и установленная служба systemd")
+ }
+ return nil
+}
+
+// restoreService применяет действие к службе FDPI при восстановлении после подбора.
+func restoreService(ctx context.Context, action string) error {
+ _, e := service.Command(ctx, action, "fdpi.service")
+ return e
+}
diff --git a/internal/diagnostics/doctor_windows.go b/internal/diagnostics/doctor_windows.go
new file mode 100644
index 0000000..23d02b6
--- /dev/null
+++ b/internal/diagnostics/doctor_windows.go
@@ -0,0 +1,66 @@
+package diagnostics
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+
+ "github.com/futureUnd1ground/fdpi/internal/config"
+ "github.com/futureUnd1ground/fdpi/internal/engine"
+ "github.com/futureUnd1ground/fdpi/internal/service"
+)
+
+func Doctor(ctx context.Context) []Check {
+ var out []Check
+ add := func(n string, e error, d string) {
+ if e != nil {
+ d = e.Error()
+ }
+ out = append(out, Check{n, e == nil, d})
+ }
+ _, e := os.Stat(engine.Path)
+ add("Файл движка", e, engine.Path)
+ for _, name := range []string{"WinDivert.dll", "WinDivert64.sys"} {
+ _, e := os.Stat(filepath.Join(filepath.Dir(engine.Path), name))
+ add("Компонент "+name, e, "драйвер WinDivert рядом с winws2.exe")
+ }
+ c, e := config.Load(service.SystemConfig)
+ add("Системные настройки", e, service.SystemConfig)
+ if e == nil {
+ dir, e := os.MkdirTemp("", "fdpi-doctor-")
+ if e == nil {
+ defer os.RemoveAll(dir)
+ a, _, _, ae := engine.Args(c, dir)
+ if ae == nil {
+ ae = engine.Check(ctx, engine.Path, a)
+ }
+ add("Совместимость движка", ae, "параметры приняты реальным движком через --dry-run")
+ }
+ }
+ add("Права администратора", func() error {
+ if !service.IsAdmin() {
+ return fmt.Errorf("обычный пользователь — это нормально для интерфейса; административные действия запросят повышение прав")
+ }
+ return nil
+ }(), "администратор")
+ _, e = net.DefaultResolver.LookupHost(ctx, "example.com")
+ add("DNS", e, "адрес example.com получен")
+ s := service.State(ctx)
+ out = append(out, Check{"Служба FDPI", s.Detail == "" && s.Installed, s.StateText()})
+ return out
+}
+
+// requireRoot проверяет полномочия для подбора стратегий (lab).
+func requireRoot() error {
+ if !service.IsAdmin() {
+ return fmt.Errorf("для подбора нужны права администратора и установленная служба FDPI")
+ }
+ return nil
+}
+
+// restoreService применяет действие к службе FDPI при восстановлении после подбора.
+func restoreService(ctx context.Context, action string) error {
+ return service.Control(ctx, action, false)
+}
diff --git a/internal/diagnostics/youtube.go b/internal/diagnostics/youtube.go
index 4b42e29..c514c09 100644
--- a/internal/diagnostics/youtube.go
+++ b/internal/diagnostics/youtube.go
@@ -12,10 +12,7 @@ import (
"net/http/httptrace"
"regexp"
"strings"
- "syscall"
"time"
-
- "golang.org/x/sys/unix"
)
type EndpointResult struct {
@@ -51,17 +48,11 @@ func YouTubeResolved(ctx context.Context, iface string, addresses map[string]str
d := net.Dialer{Timeout: 5 * time.Second}
if iface != "" {
- if _, err := net.InterfaceByName(iface); err != nil {
+ ctl, err := dialControl(iface)
+ if err != nil {
return YouTubeReport{Interface: iface, Checks: []EndpointResult{{Name: "Интерфейс", Error: err.Error()}}}
}
- d.Control = func(_, _ string, c syscall.RawConn) error {
- var err error
- e := c.Control(func(fd uintptr) { err = unix.SetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_BINDTODEVICE, iface) })
- if e != nil {
- return e
- }
- return err
- }
+ d.Control = ctl
}
dial := func(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
diff --git a/internal/engine/engine.go b/internal/engine/engine_common.go
similarity index 50%
rename from internal/engine/engine.go
rename to internal/engine/engine_common.go
index b5aff6e..4895eb4 100644
--- a/internal/engine/engine.go
+++ b/internal/engine/engine_common.go
@@ -1,21 +1,13 @@
-// Package engine compiles validated configuration into nfqws arguments, never shell text.
+// Package engine compiles validated configuration into nfqws/winws arguments, never shell text.
package engine
import (
- "context"
- "fmt"
"github.com/futureUnd1ground/fdpi/internal/config"
- "github.com/futureUnd1ground/fdpi/internal/network"
- "os"
- "os/exec"
"path/filepath"
"strconv"
"strings"
- "time"
)
-const Path = "/usr/lib/fdpi/nfqws2"
-
// Args compiles FDPI's restricted data model, never user-provided Lua text.
func Args(c config.Config, dir string) ([]string, bool, bool, error) {
return ArgsForEngine(c, dir, Path)
@@ -26,9 +18,9 @@ func ArgsForEngine(c config.Config, dir, executable string) (args []string, tcp,
return
}
libraries := filepath.Join(filepath.Dir(executable), "lua")
- args = []string{fmt.Sprintf("--qnum=%d", network.Queue), "--uid=0:0", "--fwmark=" + network.Mark, "--bind-fix4", "--bind-fix6",
- "--lua-init=@" + filepath.Join(libraries, "zapret-lib.lua"), "--lua-init=@" + filepath.Join(libraries, "zapret-antidpi.lua")}
- count := 0
+ // Группы аргументов собираются отдельно; платформенный префикс (platformArgs)
+ // вставляется перед ними, чтобы Linux-вывод остался побайтово прежним.
+ var groups [][]string
for _, p := range c.Profiles {
if !p.Enabled {
continue
@@ -39,12 +31,7 @@ func ArgsForEngine(c config.Config, dir, executable string) (args []string, tcp,
return
}
add := func(a []string) {
- if count > 0 {
- args = append(args, "--new")
- }
- args = append(args, a...)
- args = append(args, "--hostlist="+host)
- count++
+ groups = append(groups, append(a, "--hostlist="+host))
}
if s.TCP != "" || s.HostCase {
a := []string{"--filter-tcp=80,443", "--in-range=x"}
@@ -84,7 +71,16 @@ func ArgsForEngine(c config.Config, dir, executable string) (args []string, tcp,
udp = true
}
}
- if count > 0 {
+ args = append(platformArgs(tcp, udp),
+ "--lua-init=@"+filepath.Join(libraries, "zapret-lib.lua"),
+ "--lua-init=@"+filepath.Join(libraries, "zapret-antidpi.lua"))
+ for i, g := range groups {
+ if i > 0 {
+ args = append(args, "--new")
+ }
+ args = append(args, g...)
+ }
+ if len(groups) > 0 {
args = append(args, "--new", "--filter-tcp=80,443", "--filter-udp=443")
}
return
@@ -99,50 +95,3 @@ func TrustedLibraries() error {
}
return nil
}
-func Check(ctx context.Context, path string, args []string) error {
- ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
- defer cancel()
- h, e := exec.CommandContext(ctx, path, "--help").CombinedOutput()
- if e != nil && len(h) == 0 {
- return fmt.Errorf("движок: %w", e)
- }
- for _, feature := range []string{"--dry-run", "--lua-desync=", "--lua-init=", "--bind-fix4", "--bind-fix6", "--hostlist=", "--new", "--pidfile=", "--uid=", "--fwmark="} {
- if !strings.Contains(string(h), feature) {
- return fmt.Errorf("несовместимый nfqws: нет параметра %s; требуется nfqws2", feature)
- }
- }
- a := []string{"--dry-run"}
- for _, arg := range args {
- if os.Geteuid() != 0 && arg == "--uid=0:0" {
- continue
- }
- a = append(a, arg)
- }
- b, e := exec.CommandContext(ctx, path, a...).CombinedOutput()
- if e != nil {
- return fmt.Errorf("проверка параметров nfqws: %w: %s", e, b)
- }
- return nil
-}
-func Trusted(path string) error {
- // Resolve each component: root must own it and group/other must not be able to replace it.
- for p := filepath.Clean(path); ; p = filepath.Dir(p) {
- st, e := os.Lstat(p)
- if e != nil {
- return e
- }
- if st.Mode()&os.ModeSymlink != 0 {
- return fmt.Errorf("в привилегированном пути запрещена символическая ссылка: %s", p)
- }
- if st.Mode().Perm()&0022 != 0 {
- return fmt.Errorf("привилегированный путь доступен для записи группе или другим пользователям: %s", p)
- }
- if !rootOwned(st) {
- return fmt.Errorf("владельцем привилегированного пути должен быть root: %s", p)
- }
- if p == "/" {
- break
- }
- }
- return nil
-}
diff --git a/internal/engine/engine_linux.go b/internal/engine/engine_linux.go
new file mode 100644
index 0000000..7338616
--- /dev/null
+++ b/internal/engine/engine_linux.go
@@ -0,0 +1,67 @@
+package engine
+
+import (
+ "context"
+ "fmt"
+ "github.com/futureUnd1ground/fdpi/internal/network"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+const Path = "/usr/lib/fdpi/nfqws2"
+
+// platformArgs возвращает системный префикс argv движка nfqws2 (NFQUEUE).
+func platformArgs(tcp, udp bool) []string {
+ return []string{fmt.Sprintf("--qnum=%d", network.Queue), "--uid=0:0", "--fwmark=" + network.Mark, "--bind-fix4", "--bind-fix6"}
+}
+
+func Check(ctx context.Context, path string, args []string) error {
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+ h, e := exec.CommandContext(ctx, path, "--help").CombinedOutput()
+ if e != nil && len(h) == 0 {
+ return fmt.Errorf("движок: %w", e)
+ }
+ for _, feature := range []string{"--dry-run", "--lua-desync=", "--lua-init=", "--bind-fix4", "--bind-fix6", "--hostlist=", "--new", "--pidfile=", "--uid=", "--fwmark="} {
+ if !strings.Contains(string(h), feature) {
+ return fmt.Errorf("несовместимый nfqws: нет параметра %s; требуется nfqws2", feature)
+ }
+ }
+ a := []string{"--dry-run"}
+ for _, arg := range args {
+ if os.Geteuid() != 0 && arg == "--uid=0:0" {
+ continue
+ }
+ a = append(a, arg)
+ }
+ b, e := exec.CommandContext(ctx, path, a...).CombinedOutput()
+ if e != nil {
+ return fmt.Errorf("проверка параметров nfqws: %w: %s", e, b)
+ }
+ return nil
+}
+func Trusted(path string) error {
+ // Resolve each component: root must own it and group/other must not be able to replace it.
+ for p := filepath.Clean(path); ; p = filepath.Dir(p) {
+ st, e := os.Lstat(p)
+ if e != nil {
+ return e
+ }
+ if st.Mode()&os.ModeSymlink != 0 {
+ return fmt.Errorf("в привилегированном пути запрещена символическая ссылка: %s", p)
+ }
+ if st.Mode().Perm()&0022 != 0 {
+ return fmt.Errorf("привилегированный путь доступен для записи группе или другим пользователям: %s", p)
+ }
+ if !rootOwned(st) {
+ return fmt.Errorf("владельцем привилегированного пути должен быть root: %s", p)
+ }
+ if p == "/" {
+ break
+ }
+ }
+ return nil
+}
diff --git a/internal/engine/engine_windows.go b/internal/engine/engine_windows.go
new file mode 100644
index 0000000..e910de2
--- /dev/null
+++ b/internal/engine/engine_windows.go
@@ -0,0 +1,74 @@
+package engine
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// Path is winws2.exe shipped next to fdpi.exe; WinDivert filters live inside winws2 itself.
+var Path = func() string {
+ exe, e := os.Executable()
+ if e != nil {
+ return "winws2.exe"
+ }
+ if real, e := filepath.EvalSymlinks(exe); e == nil {
+ exe = real
+ }
+ return filepath.Join(filepath.Dir(exe), "winws2.exe")
+}()
+
+// platformArgs возвращает системный префикс argv движка winws2 (WinDivert).
+func platformArgs(tcp, udp bool) []string {
+ var args []string
+ if tcp {
+ args = append(args, "--wf-tcp-out=80,443")
+ }
+ if udp {
+ args = append(args, "--wf-udp-out=443")
+ }
+ return append(args, "--wf-dup-check")
+}
+
+func Check(ctx context.Context, path string, args []string) error {
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+ h, e := exec.CommandContext(ctx, path, "--help").CombinedOutput()
+ if e != nil && len(h) == 0 {
+ return fmt.Errorf("движок: %w", e)
+ }
+ for _, feature := range []string{"--dry-run", "--lua-desync=", "--lua-init=", "--hostlist=", "--new", "--pidfile=", "--wf-tcp-out="} {
+ if !strings.Contains(string(h), feature) {
+ return fmt.Errorf("несовместимый winws: нет параметра %s; требуется winws2", feature)
+ }
+ }
+ a := append([]string{"--dry-run"}, args...)
+ b, e := exec.CommandContext(ctx, path, a...).CombinedOutput()
+ if e != nil {
+ return fmt.Errorf("проверка параметров winws2: %w: %s", e, b)
+ }
+ return nil
+}
+
+// Trusted rejects missing paths and any symlink component; Windows has no root owner.
+func Trusted(path string) error {
+ for p := filepath.Clean(path); ; {
+ st, e := os.Lstat(p)
+ if e != nil {
+ return e
+ }
+ if st.Mode()&os.ModeSymlink != 0 {
+ return fmt.Errorf("в привилегированном пути запрещена символическая ссылка: %s", p)
+ }
+ parent := filepath.Dir(p)
+ if parent == p {
+ break
+ }
+ p = parent
+ }
+ return nil
+}
diff --git a/internal/engine/engine_windows_test.go b/internal/engine/engine_windows_test.go
new file mode 100644
index 0000000..969c792
--- /dev/null
+++ b/internal/engine/engine_windows_test.go
@@ -0,0 +1,57 @@
+package engine
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/futureUnd1ground/fdpi/internal/config"
+)
+
+// Тесты компилируются и выполняются под GOOS=windows; под Linux здесь
+// проверяется только компиляция (go vet/build с GOOS=windows).
+
+func TestWindowsPlatformArgs(t *testing.T) {
+ if got := platformArgs(false, false); len(got) != 1 || got[0] != "--wf-dup-check" {
+ t.Fatal(got)
+ }
+ got := platformArgs(true, false)
+ if len(got) != 2 || got[1] != "--wf-tcp-out=80,443" {
+ t.Fatal(got)
+ }
+ got = platformArgs(true, true)
+ if len(got) != 3 || got[2] != "--wf-udp-out=443" {
+ t.Fatal(got)
+ }
+}
+
+func TestWindowsArgsOrder(t *testing.T) {
+ c := config.Default()
+ c.Profiles[0].Enabled = true
+ c.Profiles[0].Strategy = "aggressive" // TCP + QUIC
+ a, tcp, udp, e := Args(c, t.TempDir())
+ if e != nil || !tcp || !udp {
+ t.Fatalf("%v %v %v", e, tcp, udp)
+ }
+ joined := strings.Join(a, " ")
+ // Платформенный префикс идёт первым и до первого --new.
+ firstNew := strings.Index(joined, "--new")
+ if firstNew < 0 {
+ t.Fatal("no groups")
+ }
+ prefix := joined[:firstNew]
+ for _, want := range []string{"--wf-dup-check", "--wf-tcp-out=80,443", "--wf-udp-out=443", "--lua-init=@", "zapret-lib.lua", "zapret-antidpi.lua"} {
+ if !strings.Contains(prefix, want) {
+ t.Fatalf("префикс без %q: %s", want, prefix)
+ }
+ }
+ if strings.Contains(prefix, "--qnum") || strings.Contains(prefix, "--uid=") || strings.Contains(prefix, "--fwmark") {
+ t.Fatalf("linux-параметры в windows-argv: %s", prefix)
+ }
+ // Финальная catch-all группа — в конце.
+ if a[len(a)-1] != "--filter-udp=443" || a[len(a)-3] != "--new" {
+ t.Fatal(a)
+ }
+ if !strings.HasSuffix(joined, "--new --filter-tcp=80,443 --filter-udp=443") {
+ t.Fatal(joined)
+ }
+}
diff --git a/internal/engine/owner_windows.go b/internal/engine/owner_windows.go
new file mode 100644
index 0000000..4978a82
--- /dev/null
+++ b/internal/engine/owner_windows.go
@@ -0,0 +1,6 @@
+package engine
+
+import "os"
+
+// rootOwned has no meaning on Windows; existence and symlink checks are done in Trusted.
+func rootOwned(os.FileInfo) bool { return true }
diff --git a/internal/network/network.go b/internal/network/network_linux.go
similarity index 100%
rename from internal/network/network.go
rename to internal/network/network_linux.go
diff --git a/internal/network/network_test.go b/internal/network/network_linux_test.go
similarity index 100%
rename from internal/network/network_test.go
rename to internal/network/network_linux_test.go
diff --git a/internal/network/network_windows.go b/internal/network/network_windows.go
new file mode 100644
index 0000000..debdf2f
--- /dev/null
+++ b/internal/network/network_windows.go
@@ -0,0 +1,13 @@
+// Package network on Windows is inert: the WinDivert filter lives and dies inside winws2.exe.
+package network
+
+import "context"
+
+const Table = "fdpi"
+const Queue = 24000
+const Mark = "0x20000000"
+const Owner = "fdpi-managed-v1"
+
+func Owned(context.Context) (bool, error) { return false, nil }
+func Apply(context.Context, bool, bool) error { return nil }
+func Cleanup(context.Context) error { return nil }
diff --git a/internal/service/accounting.go b/internal/service/accounting_linux.go
similarity index 100%
rename from internal/service/accounting.go
rename to internal/service/accounting_linux.go
diff --git a/internal/service/accounting_test.go b/internal/service/accounting_linux_test.go
similarity index 100%
rename from internal/service/accounting_test.go
rename to internal/service/accounting_linux_test.go
diff --git a/internal/service/accounting_windows.go b/internal/service/accounting_windows.go
new file mode 100644
index 0000000..7bbd4a6
--- /dev/null
+++ b/internal/service/accounting_windows.go
@@ -0,0 +1,9 @@
+package service
+
+import "context"
+
+// EnableAccounting is a no-op on Windows: winws2 keeps its own connection tracking.
+func EnableAccounting(ctx context.Context) error { return nil }
+
+// PersistAccounting is a no-op on Windows: no sysctl persistence exists.
+func PersistAccounting() error { return nil }
diff --git a/internal/service/conflicts.go b/internal/service/conflicts_linux.go
similarity index 94%
rename from internal/service/conflicts.go
rename to internal/service/conflicts_linux.go
index ede1cc3..e875406 100644
--- a/internal/service/conflicts.go
+++ b/internal/service/conflicts_linux.go
@@ -9,8 +9,6 @@ import (
"time"
)
-// ErrConflict denotes a permanent conflict that restarting the daemon cannot fix.
-var ErrConflict = errors.New("конфликт сетевых движков")
var otherUnits = []string{"zapret.service", "zapret2.service"}
func OtherActive(ctx context.Context) []string {
diff --git a/internal/service/conflicts_test.go b/internal/service/conflicts_linux_test.go
similarity index 100%
rename from internal/service/conflicts_test.go
rename to internal/service/conflicts_linux_test.go
diff --git a/internal/service/conflicts_windows.go b/internal/service/conflicts_windows.go
new file mode 100644
index 0000000..cb95771
--- /dev/null
+++ b/internal/service/conflicts_windows.go
@@ -0,0 +1,205 @@
+package service
+
+import (
+ "context"
+ "fmt"
+ "github.com/futureUnd1ground/fdpi/internal/engine"
+ "path/filepath"
+ "strings"
+ "time"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+ "golang.org/x/sys/windows/svc"
+ "golang.org/x/sys/windows/svc/mgr"
+)
+
+// Foreign DPI engines detected by process image name; our own winws2.exe is excluded by path.
+var foreignImages = map[string]bool{
+ "winws.exe": true,
+ "winws2.exe": true,
+ "goodbyedpi.exe": true,
+ "byedpi.exe": true,
+}
+
+type processEntry32 struct {
+ dwSize uint32
+ cntUsage uint32
+ th32ProcessID uint32
+ th32DefaultHeapID uintptr
+ th32ModuleID uint32
+ cntThreads uint32
+ th32ParentProcessID uint32
+ pcPriClassBase int32
+ dwFlags uint32
+ szExeFile [windows.MAX_PATH]uint16
+}
+
+var (
+ procCreateToolhelp32Snapshot = windows.NewLazySystemDLL("kernel32.dll").NewProc("CreateToolhelp32Snapshot")
+ procProcess32FirstW = windows.NewLazySystemDLL("kernel32.dll").NewProc("Process32FirstW")
+ procProcess32NextW = windows.NewLazySystemDLL("kernel32.dll").NewProc("Process32NextW")
+)
+
+func snapshotProcesses() map[uint32]string {
+ out := map[uint32]string{}
+ h, _, _ := procCreateToolhelp32Snapshot.Call(0x2 /* TH32CS_SNAPPROCESS */, 0)
+ if h == 0 || h == uintptr(windows.InvalidHandle) {
+ return out
+ }
+ defer windows.CloseHandle(windows.Handle(h))
+ var entry processEntry32
+ entry.dwSize = uint32(unsafe.Sizeof(entry))
+ ok, _, _ := procProcess32FirstW.Call(h, uintptr(unsafe.Pointer(&entry)))
+ for ok != 0 {
+ out[entry.th32ProcessID] = windows.UTF16ToString(entry.szExeFile[:])
+ ok, _, _ = procProcess32NextW.Call(h, uintptr(unsafe.Pointer(&entry)))
+ }
+ return out
+}
+
+func isForeignImage(name string) bool {
+ name = strings.ToLower(name)
+ if foreignImages[name] {
+ return true
+ }
+ return strings.HasPrefix(name, "zapret") && strings.HasSuffix(name, ".exe")
+}
+
+// sameImagePath compares process image paths case-insensitively, ignoring symlinks.
+func sameImagePath(a, b string) bool {
+ clean := func(p string) string {
+ if real, e := filepath.EvalSymlinks(p); e == nil {
+ p = real
+ }
+ return strings.ToLower(filepath.Clean(p))
+ }
+ return clean(a) == clean(b)
+}
+
+func processImagePath(pid uint32) (string, bool) {
+ h, e := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
+ if e != nil {
+ return "", false
+ }
+ defer windows.CloseHandle(h)
+ var buf [windows.MAX_PATH]uint16
+ n := uint32(len(buf))
+ if e = windows.QueryFullProcessImageName(h, 0, &buf[0], &n); e != nil {
+ return "", false
+ }
+ return windows.UTF16ToString(buf[:n]), true
+}
+
+func foreignProcesses() map[uint32]string {
+ out := map[uint32]string{}
+ for pid, name := range snapshotProcesses() {
+ if !isForeignImage(name) {
+ continue
+ }
+ if path, ok := processImagePath(pid); ok && sameImagePath(path, engine.Path) {
+ continue // наш собственный winws2.exe рядом с fdpi.exe
+ }
+ out[pid] = name
+ }
+ return out
+}
+
+func sortedNames(procs map[uint32]string) []string {
+ var out []string
+ seen := map[string]bool{}
+ for _, name := range procs {
+ if !seen[name] {
+ seen[name] = true
+ out = append(out, name)
+ }
+ }
+ return out
+}
+
+func OtherActive(ctx context.Context) []string { return sortedNames(foreignProcesses()) }
+
+func terminateProcess(pid uint32) {
+ h, e := windows.OpenProcess(windows.PROCESS_TERMINATE, false, pid)
+ if e != nil {
+ return
+ }
+ defer windows.CloseHandle(h)
+ _ = windows.TerminateProcess(h, 1)
+}
+
+func openFdpiService() (*mgr.Mgr, *mgr.Service, error) {
+ m, e := mgr.Connect()
+ if e != nil {
+ return nil, nil, fmt.Errorf("подключение к диспетчеру служб: %w", e)
+ }
+ s, e := m.OpenService(serviceName)
+ if e != nil {
+ m.Disconnect()
+ return nil, nil, fmt.Errorf("служба fdpi не установлена; выполните fdpi setup: %w", e)
+ }
+ return m, s, nil
+}
+
+func stopFdpiService(s *mgr.Service) error {
+ st, e := s.Query()
+ if e != nil {
+ return e
+ }
+ if st.State == svc.Stopped {
+ return nil
+ }
+ if _, e = s.Control(svc.Stop); e != nil {
+ return fmt.Errorf("остановка службы fdpi: %w", e)
+ }
+ return waitState(s, svc.Stopped, 15*time.Second)
+}
+
+// Control only replaces foreign engines on an explicit --replace request.
+func Control(ctx context.Context, action string, replace bool) error {
+ if action == "start" || action == "restart" {
+ foreign := foreignProcesses()
+ if len(foreign) > 0 {
+ names := sortedNames(foreign)
+ if !replace {
+ return fmt.Errorf("%w: %s. Откройте fdpi и подтвердите замену либо выполните fdpi %s --replace; чужие процессы автоматически не остановлены", ErrConflict, strings.Join(names, ", "), action)
+ }
+ for pid := range foreign {
+ terminateProcess(pid)
+ }
+ }
+ }
+ m, s, e := openFdpiService()
+ if e != nil {
+ return e
+ }
+ defer m.Disconnect()
+ defer s.Close()
+ switch action {
+ case "start":
+ return s.Start()
+ case "stop":
+ return stopFdpiService(s)
+ case "restart":
+ if e = stopFdpiService(s); e != nil {
+ return e
+ }
+ return s.Start()
+ case "enable":
+ cfg, e := s.Config()
+ if e != nil {
+ return e
+ }
+ cfg.StartType = mgr.StartAutomatic
+ return s.UpdateConfig(cfg)
+ case "disable":
+ cfg, e := s.Config()
+ if e != nil {
+ return e
+ }
+ cfg.StartType = mgr.StartManual
+ return s.UpdateConfig(cfg)
+ default:
+ return fmt.Errorf("неизвестное действие службы: %s", action)
+ }
+}
diff --git a/internal/service/payload_test.go b/internal/service/payload_linux_test.go
similarity index 100%
rename from internal/service/payload_test.go
rename to internal/service/payload_linux_test.go
diff --git a/internal/service/service_common.go b/internal/service/service_common.go
new file mode 100644
index 0000000..86df9c4
--- /dev/null
+++ b/internal/service/service_common.go
@@ -0,0 +1,73 @@
+// Package service supervises the DPI engine and restores the last accepted configuration.
+package service
+
+import (
+ "encoding/json"
+ "errors"
+ "github.com/futureUnd1ground/fdpi/internal/config"
+)
+
+// ErrConflict denotes a permanent conflict that restarting the daemon cannot fix.
+var ErrConflict = errors.New("конфликт сетевых движков")
+
+func commit(path, backup string, c config.Config, active bool, restart, restore func() error) error {
+ old, e := config.Load(path)
+ if e != nil {
+ return e
+ }
+ if e = config.Save(backup, old); e != nil {
+ return e
+ }
+ if e = config.Save(path, c); e != nil {
+ return e
+ }
+ if active {
+ if e = restart(); e != nil {
+ return errors.Join(e, config.Save(path, old), restore())
+ }
+ }
+ return nil
+}
+
+type Status struct {
+ Installed bool `json:"installed"`
+ QueueObservable bool `json:"queue_observable"`
+ Service string `json:"service"`
+ UnitFileState string `json:"unit_file_state,omitempty"`
+ SupervisorPID int `json:"supervisor_pid,omitempty"`
+ QueueBound bool `json:"queue_bound"`
+ Detail string `json:"detail,omitempty"`
+}
+
+func JSON(v any) string { b, _ := json.MarshalIndent(v, "", " "); return string(b) }
+
+func (s Status) QueueText() string {
+ if !s.QueueObservable {
+ return "неизвестно (нужны права root)"
+ }
+ if s.QueueBound {
+ return "подключена"
+ }
+ return "не подключена"
+}
+
+// StateText renders service states for people; JSON keeps the stable identifiers.
+func (s Status) StateText() string {
+ switch s.Service {
+ case "active":
+ return "работает"
+ case "inactive":
+ return "остановлена"
+ case "failed":
+ return "ошибка запуска"
+ case "activating":
+ return "запускается"
+ case "deactivating":
+ return "останавливается"
+ case "not-installed":
+ return "не установлена — выполните fdpi setup"
+ case "unknown":
+ return "состояние неизвестно"
+ }
+ return s.Service
+}
diff --git a/internal/service/service.go b/internal/service/service_linux.go
similarity index 79%
rename from internal/service/service.go
rename to internal/service/service_linux.go
index 7715426..27ac23d 100644
--- a/internal/service/service.go
+++ b/internal/service/service_linux.go
@@ -3,7 +3,6 @@ package service
import (
"context"
- "encoding/json"
"errors"
"fmt"
"github.com/futureUnd1ground/fdpi/internal/config"
@@ -21,6 +20,13 @@ import (
)
const SystemConfig = "/etc/fdpi/config.json"
+const PreviousConfig = "/etc/fdpi/previous.json"
+
+// OperationLockPath сериализует административные операции FDPI.
+func OperationLockPath() string { return "/etc/fdpi/.operation.lock" }
+
+// IsAdmin reports whether the process runs with root privileges.
+func IsAdmin() bool { return os.Geteuid() == 0 }
func Command(ctx context.Context, args ...string) (string, error) {
b, e := exec.CommandContext(ctx, "/usr/bin/systemctl", args...).CombinedOutput()
@@ -212,32 +218,13 @@ func Apply(ctx context.Context, path string) error {
if state.Detail != "" || !state.Installed {
return fmt.Errorf("не удалось проверить службу systemd: %s", state.Detail)
}
- return commit(SystemConfig, "/etc/fdpi/previous.json", c, state.Service == "active", func() error { return RestartCheck(ctx) }, func() error {
+ return commit(SystemConfig, PreviousConfig, c, state.Service == "active", func() error { return RestartCheck(ctx) }, func() error {
restore, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
return RestartCheck(restore)
})
}
-func commit(path, backup string, c config.Config, active bool, restart, restore func() error) error {
- old, e := config.Load(path)
- if e != nil {
- return e
- }
- if e = config.Save(backup, old); e != nil {
- return e
- }
- if e = config.Save(path, c); e != nil {
- return e
- }
- if active {
- if e = restart(); e != nil {
- return errors.Join(e, config.Save(path, old), restore())
- }
- }
- return nil
-}
-
func RestartCheck(ctx context.Context) error {
if e := Control(ctx, "restart", false); e != nil {
return e
@@ -256,16 +243,6 @@ func RestartCheck(ctx context.Context) error {
return nil
}
-type Status struct {
- Installed bool `json:"installed"`
- QueueObservable bool `json:"queue_observable"`
- Service string `json:"service"`
- UnitFileState string `json:"unit_file_state,omitempty"`
- SupervisorPID int `json:"supervisor_pid,omitempty"`
- QueueBound bool `json:"queue_bound"`
- Detail string `json:"detail,omitempty"`
-}
-
func State(ctx context.Context) Status {
s, e := Command(ctx, "show", "fdpi.service", "--property=ActiveState,MainPID,LoadState,UnitFileState")
_, qe := os.ReadFile("/proc/net/netfilter/nfnetlink_queue")
@@ -295,7 +272,6 @@ func State(ctx context.Context) Status {
}
return v
}
-func JSON(v any) string { b, _ := json.MarshalIndent(v, "", " "); return string(b) }
func notifyReady() {
path := os.Getenv("NOTIFY_SOCKET")
@@ -309,34 +285,3 @@ func notifyReady() {
defer conn.Close()
_, _ = conn.Write([]byte("READY=1"))
}
-
-func (s Status) QueueText() string {
- if !s.QueueObservable {
- return "неизвестно (нужны права root)"
- }
- if s.QueueBound {
- return "подключена"
- }
- return "не подключена"
-}
-
-// StateText renders systemd states for people; JSON keeps the stable systemd identifiers.
-func (s Status) StateText() string {
- switch s.Service {
- case "active":
- return "работает"
- case "inactive":
- return "остановлена"
- case "failed":
- return "ошибка запуска"
- case "activating":
- return "запускается"
- case "deactivating":
- return "останавливается"
- case "not-installed":
- return "не установлена — выполните fdpi setup"
- case "unknown":
- return "состояние неизвестно"
- }
- return s.Service
-}
diff --git a/internal/service/service_test.go b/internal/service/service_linux_test.go
similarity index 100%
rename from internal/service/service_test.go
rename to internal/service/service_linux_test.go
diff --git a/internal/service/service_windows.go b/internal/service/service_windows.go
new file mode 100644
index 0000000..f8c9c30
--- /dev/null
+++ b/internal/service/service_windows.go
@@ -0,0 +1,415 @@
+// Package service supervises winws2 and restores the last accepted configuration.
+package service
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/futureUnd1ground/fdpi/internal/config"
+ "github.com/futureUnd1ground/fdpi/internal/engine"
+ "io"
+ "os"
+ "os/exec"
+ "os/signal"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+ "golang.org/x/sys/windows/svc"
+ "golang.org/x/sys/windows/svc/mgr"
+)
+
+const serviceName = "fdpi"
+const serviceDisplay = "FDPI — обход DPI"
+const serviceDescription = "Обход DPI через winws2 (WinDivert); управляется FDPI"
+
+// SystemConfig and PreviousConfig live under %ProgramData%\FDPI by default.
+var SystemConfig = filepath.Join(StateDir(), "config.json")
+var PreviousConfig = filepath.Join(StateDir(), "previous.json")
+
+// StateDir is %ProgramData%\FDPI, or data\ next to fdpi.exe when ProgramData is unset.
+func StateDir() string {
+ if pd := os.Getenv("ProgramData"); pd != "" {
+ return filepath.Join(pd, "FDPI")
+ }
+ exe, e := os.Executable()
+ if e != nil {
+ return "data"
+ }
+ return filepath.Join(filepath.Dir(exe), "data")
+}
+func LogsDir() string { return filepath.Join(StateDir(), "logs") }
+
+// OperationLockPath сериализует административные операции FDPI.
+func OperationLockPath() string { return filepath.Join(StateDir(), ".operation.lock") }
+func LogPath() string { return filepath.Join(LogsDir(), "engine.log") }
+func EnsureStateDir() error {
+ if e := os.MkdirAll(LogsDir(), 0755); e != nil {
+ return e
+ }
+ return nil
+}
+
+// IsAdmin reports whether the process token is elevated.
+func IsAdmin() bool {
+ var token windows.Token
+ if e := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &token); e != nil {
+ return false
+ }
+ defer token.Close()
+ var elevation uint32
+ var out uint32
+ e := windows.GetTokenInformation(token, windows.TokenElevation, (*byte)(unsafe.Pointer(&elevation)), 4, &out)
+ return e == nil && elevation != 0
+}
+
+func Lock(path string) (*os.File, error) {
+ f, e := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
+ if e != nil {
+ return nil, e
+ }
+ var overlapped windows.Overlapped
+ e = windows.LockFileEx(windows.Handle(f.Fd()), windows.LOCKFILE_FAIL_IMMEDIATELY|windows.LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &overlapped)
+ if e != nil {
+ f.Close()
+ return nil, fmt.Errorf("другая операция FDPI уже использует блокировку %s", path)
+ }
+ return f, nil
+}
+func Unlock(f *os.File) {
+ var overlapped windows.Overlapped
+ _ = windows.UnlockFileEx(windows.Handle(f.Fd()), 0, 1, 0, &overlapped)
+ _ = f.Close()
+}
+
+func Run(ctx context.Context, path string, requireTrusted bool) (err error) {
+ if foreign := OtherActive(ctx); len(foreign) > 0 {
+ return fmt.Errorf("%w: активны %s; подтвердите их остановку в fdpi", ErrConflict, strings.Join(foreign, ", "))
+ }
+ if requireTrusted {
+ if err = engine.Trusted(path); err != nil {
+ return
+ }
+ if err = engine.TrustedLibraries(); err != nil {
+ return
+ }
+ if err = engine.Trusted(engine.Path); err != nil {
+ return
+ }
+ }
+ if err = EnsureStateDir(); err != nil {
+ return
+ }
+ f, e := Lock(filepath.Join(StateDir(), "engine.lock"))
+ if e != nil {
+ return e
+ }
+ defer Unlock(f)
+ c, e := config.Load(path)
+ if e != nil {
+ return e
+ }
+ return Supervise(ctx, c, engine.Path, StateDir())
+}
+
+func Supervise(ctx context.Context, c config.Config, enginePath, dir string) (err error) {
+ a, tcp, udp, e := engine.ArgsForEngine(c, dir, enginePath)
+ if e != nil {
+ return e
+ }
+ if e = engine.Check(ctx, enginePath, a); e != nil {
+ return e
+ }
+ if !tcp && !udp {
+ notifyReady()
+ <-ctx.Done()
+ return nil
+ }
+ if e = EnsureStateDir(); e != nil {
+ return e
+ }
+ pidfile := filepath.Join(dir, "nfqws.pid")
+ if e = os.Remove(pidfile); e != nil && !errors.Is(e, os.ErrNotExist) {
+ return e
+ }
+ defer os.Remove(pidfile)
+ logFile, e := os.OpenFile(LogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+ if e != nil {
+ return e
+ }
+ defer logFile.Close()
+ child := exec.Command(enginePath, append(a, "--pidfile="+pidfile)...)
+ out := io.MultiWriter(logFile, os.Stdout)
+ child.Stdout = out
+ child.Stderr = out
+ if e = child.Start(); e != nil {
+ return e
+ }
+ done := make(chan error, 1)
+ go func() { done <- child.Wait() }()
+ stopped := false
+ defer func() {
+ if !stopped {
+ _ = child.Process.Kill()
+ <-done
+ }
+ }()
+ deadline := time.NewTimer(5 * time.Second)
+ defer deadline.Stop()
+ tick := time.NewTicker(25 * time.Millisecond)
+ defer tick.Stop()
+ ready := false
+ for !ready {
+ select {
+ case e = <-done:
+ stopped = true
+ return fmt.Errorf("winws2 завершился до готовности: %v", e)
+ case <-ctx.Done():
+ return nil
+ case <-deadline.C:
+ return fmt.Errorf("истекло время ожидания готовности winws2")
+ case <-tick.C:
+ b, e := os.ReadFile(pidfile)
+ ready = e == nil && strings.TrimSpace(string(b)) == strconv.Itoa(child.Process.Pid)
+ }
+ }
+ notifyReady()
+ fmt.Printf("Обработка FDPI включена: WinDivert фильтр в winws2, журнал %s\n", LogPath())
+ select {
+ case <-ctx.Done():
+ return nil
+ case e = <-done:
+ stopped = true
+ return fmt.Errorf("winws2 завершился: %v", e)
+ }
+}
+
+// Daemon runs under the Service Control Manager when invoked by SCM, otherwise directly.
+func Daemon() error {
+ isService, e := svc.IsWindowsService()
+ if e != nil {
+ return e
+ }
+ if !isService {
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
+ defer cancel()
+ return Run(ctx, SystemConfig, true)
+ }
+ return svc.Run(serviceName, &daemonHandler{})
+}
+
+type daemonHandler struct{}
+
+func (h *daemonHandler) Execute(_ []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ done := make(chan struct{})
+ go func() {
+ defer close(done)
+ _ = Run(ctx, SystemConfig, true)
+ }()
+ status <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
+ for {
+ select {
+ case c := <-r:
+ if c.Cmd == svc.Stop || c.Cmd == svc.Shutdown {
+ cancel()
+ select {
+ case <-done:
+ case <-time.After(20 * time.Second):
+ }
+ status <- svc.Status{State: svc.Stopped}
+ return false, 0
+ }
+ case <-done:
+ status <- svc.Status{State: svc.Stopped}
+ return false, 0
+ }
+ }
+}
+
+// Install registers the fdpi service with automatic start; existing service is reconfigured.
+func Install(ctx context.Context) error {
+ exe, e := os.Executable()
+ if e != nil {
+ return e
+ }
+ if exe, e = filepath.Abs(exe); e != nil {
+ return e
+ }
+ m, e := mgr.Connect()
+ if e != nil {
+ return fmt.Errorf("подключение к диспетчеру служб: %w", e)
+ }
+ defer m.Disconnect()
+ if s, e := m.OpenService(serviceName); e == nil {
+ defer s.Close()
+ cfg, e := s.Config()
+ if e != nil {
+ return e
+ }
+ cfg.DisplayName = serviceDisplay
+ cfg.Description = serviceDescription
+ cfg.StartType = mgr.StartAutomatic
+ return s.UpdateConfig(cfg)
+ }
+ s, e := m.CreateService(serviceName, exe, mgr.Config{
+ DisplayName: serviceDisplay,
+ Description: serviceDescription,
+ StartType: mgr.StartAutomatic,
+ }, "daemon")
+ if e != nil {
+ return fmt.Errorf("создание службы fdpi: %w", e)
+ }
+ return s.Close()
+}
+
+// Remove stops and deletes the fdpi service.
+func Remove(ctx context.Context) error {
+ m, e := mgr.Connect()
+ if e != nil {
+ return fmt.Errorf("подключение к диспетчеру служб: %w", e)
+ }
+ defer m.Disconnect()
+ s, e := m.OpenService(serviceName)
+ if e != nil {
+ return fmt.Errorf("служба fdpi не установлена: %w", e)
+ }
+ defer s.Close()
+ if st, e := s.Query(); e == nil && st.State != svc.Stopped {
+ if _, e = s.Control(svc.Stop); e == nil {
+ _ = waitState(s, svc.Stopped, 15*time.Second)
+ }
+ }
+ return s.Delete()
+}
+
+func Apply(ctx context.Context, path string) error {
+ if !IsAdmin() {
+ return fmt.Errorf("для административных действий нужны права администратора")
+ }
+ if e := RequireCurrentBinary(ctx); e != nil {
+ return e
+ }
+ if e := engine.TrustedLibraries(); e != nil {
+ return e
+ }
+ if e := engine.Trusted(engine.Path); e != nil {
+ return e
+ }
+ if e := EnsureStateDir(); e != nil {
+ return e
+ }
+ f, e := Lock(filepath.Join(StateDir(), ".operation.lock"))
+ if e != nil {
+ return e
+ }
+ defer Unlock(f)
+ c, e := config.Load(path)
+ if e != nil {
+ return e
+ }
+ dir, e := os.MkdirTemp(StateDir(), ".validate-")
+ if e != nil {
+ return e
+ }
+ defer os.RemoveAll(dir)
+ a, _, _, e := engine.Args(c, dir)
+ if e != nil {
+ return e
+ }
+ if e = engine.Check(ctx, engine.Path, a); e != nil {
+ return e
+ }
+ state := State(ctx)
+ if state.Detail != "" || !state.Installed {
+ return fmt.Errorf("не удалось проверить службу fdpi: %s", state.Detail)
+ }
+ return commit(SystemConfig, PreviousConfig, c, state.Service == "active", func() error { return RestartCheck(ctx) }, func() error {
+ restore, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+ defer cancel()
+ return RestartCheck(restore)
+ })
+}
+
+func RestartCheck(ctx context.Context) error {
+ if e := Control(ctx, "restart", false); e != nil {
+ return e
+ }
+ deadline := time.Now().Add(10 * time.Second)
+ for {
+ st := State(ctx)
+ if st.Detail == "" && st.Service == "active" {
+ return nil
+ }
+ if time.Now().After(deadline) {
+ return fmt.Errorf("служба не запустилась после перезапуска: %s", st.StateText())
+ }
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-time.After(400 * time.Millisecond):
+ }
+ }
+}
+
+func State(ctx context.Context) Status {
+ v := Status{Service: "unknown"}
+ m, e := mgr.Connect()
+ if e != nil {
+ v.Detail = e.Error()
+ return v
+ }
+ defer m.Disconnect()
+ v.QueueObservable = true
+ s, e := m.OpenService(serviceName)
+ if e != nil {
+ v.Service = "not-installed"
+ return v
+ }
+ defer s.Close()
+ v.Installed = true
+ st, e := s.Query()
+ if e != nil {
+ v.Detail = e.Error()
+ return v
+ }
+ switch st.State {
+ case svc.Running:
+ v.Service = "active"
+ case svc.Stopped:
+ v.Service = "inactive"
+ case svc.StartPending:
+ v.Service = "activating"
+ case svc.StopPending:
+ v.Service = "deactivating"
+ }
+ pidFile := filepath.Join(StateDir(), "nfqws.pid")
+ if b, e := os.ReadFile(pidFile); e == nil {
+ v.SupervisorPID, _ = strconv.Atoi(strings.TrimSpace(string(b)))
+ v.QueueBound = v.Service == "active"
+ }
+ return v
+}
+
+func waitState(s *mgr.Service, want svc.State, timeout time.Duration) error {
+ deadline := time.Now().Add(timeout)
+ for {
+ st, e := s.Query()
+ if e != nil {
+ return e
+ }
+ if st.State == want {
+ return nil
+ }
+ if time.Now().After(deadline) {
+ return fmt.Errorf("истекло время ожидания состояния службы %d", want)
+ }
+ time.Sleep(300 * time.Millisecond)
+ }
+}
+
+func notifyReady() {}
diff --git a/internal/service/version.go b/internal/service/version_linux.go
similarity index 100%
rename from internal/service/version.go
rename to internal/service/version_linux.go
diff --git a/internal/service/version_windows.go b/internal/service/version_windows.go
new file mode 100644
index 0000000..091e30d
--- /dev/null
+++ b/internal/service/version_windows.go
@@ -0,0 +1,59 @@
+package service
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/sys/windows/svc/mgr"
+)
+
+// RequireCurrentBinary prevents mixing a new controller with an old installed service.
+func RequireCurrentBinary(ctx context.Context) error {
+ m, e := mgr.Connect()
+ if e != nil {
+ return fmt.Errorf("подключение к диспетчеру служб: %w", e)
+ }
+ defer m.Disconnect()
+ s, e := m.OpenService(serviceName)
+ if e != nil {
+ return fmt.Errorf("системная часть FDPI не установлена; откройте установщик клавишей i или выполните fdpi setup: %w", e)
+ }
+ defer s.Close()
+ cfg, e := s.Config()
+ if e != nil {
+ return e
+ }
+ installed := serviceBinaryPath(cfg.BinaryPathName)
+ current, e := os.Executable()
+ if e != nil {
+ return e
+ }
+ canonical := func(p string) string {
+ if real, e := filepath.EvalSymlinks(p); e == nil {
+ p = real
+ }
+ return strings.ToLower(filepath.Clean(p))
+ }
+ if canonical(installed) != canonical(current) {
+ return fmt.Errorf("служба fdpi использует другой исполняемый файл: %s; текущий: %s. Переустановите службу: fdpi setup", installed, current)
+ }
+ return nil
+}
+
+// serviceBinaryPath extracts the executable from a command line like "C:\...\fdpi.exe" daemon.
+func serviceBinaryPath(commandLine string) string {
+ commandLine = strings.TrimSpace(commandLine)
+ if strings.HasPrefix(commandLine, "\"") {
+ if i := strings.Index(commandLine[1:], "\""); i >= 0 {
+ return commandLine[1 : 1+i]
+ }
+ return strings.Trim(commandLine, "\"")
+ }
+ if i := strings.IndexByte(commandLine, ' '); i >= 0 {
+ return commandLine[:i]
+ }
+ return commandLine
+}
diff --git a/internal/tui/platform_linux.go b/internal/tui/platform_linux.go
new file mode 100644
index 0000000..9265d57
--- /dev/null
+++ b/internal/tui/platform_linux.go
@@ -0,0 +1,23 @@
+package tui
+
+import (
+ "context"
+ "fmt"
+ "os/exec"
+
+ "github.com/futureUnd1ground/fdpi/internal/network"
+)
+
+// operationCommand выполняет административную команду FDPI через sudo.
+func operationCommand(args []string) *exec.Cmd {
+ return exec.Command("sudo", append([]string{"--", "/usr/bin/fdpi"}, args...)...)
+}
+
+// recentLogs читает последние записи журнала службы через journalctl.
+func recentLogs(ctx context.Context) (string, error) {
+ b, e := exec.CommandContext(ctx, "/usr/bin/journalctl", "-u", "fdpi.service", "-n", "80", "--no-pager", "-o", "short").CombinedOutput()
+ return string(b), e
+}
+
+// engineLabel — короткое название механизма перехвата для страницы «Сеть».
+func engineLabel() string { return fmt.Sprintf("NFQUEUE %d", network.Queue) }
diff --git a/internal/tui/platform_windows.go b/internal/tui/platform_windows.go
new file mode 100644
index 0000000..2039406
--- /dev/null
+++ b/internal/tui/platform_windows.go
@@ -0,0 +1,39 @@
+package tui
+
+import (
+ "context"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/futureUnd1ground/fdpi/internal/service"
+)
+
+// operationCommand повторно запускает fdpi.exe с аргументами команды;
+// повышение прав (UAC) запросит сама команда через ShellExecute runas.
+func operationCommand(args []string) *exec.Cmd {
+ exe, e := os.Executable()
+ if e != nil {
+ exe = "fdpi.exe"
+ }
+ return exec.Command(exe, args...)
+}
+
+// recentLogs читает последние записи журнала движка из файла.
+func recentLogs(ctx context.Context) (string, error) {
+ b, e := os.ReadFile(service.LogPath())
+ if e != nil {
+ if os.IsNotExist(e) {
+ return "журнал пуст; служба ещё не запускалась", nil
+ }
+ return "", e
+ }
+ lines := strings.Split(strings.TrimRight(string(b), "\n"), "\n")
+ if len(lines) > 80 {
+ lines = lines[len(lines)-80:]
+ }
+ return strings.Join(lines, "\n"), nil
+}
+
+// engineLabel — короткое название механизма перехвата для страницы «Сеть».
+func engineLabel() string { return "WinDivert" }
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index b09f9b2..4c4b057 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -71,7 +71,7 @@ func operation(args []string) tea.Cmd {
}
}
var output operationOutput
- cmd := exec.Command("sudo", append([]string{"--", "/usr/bin/fdpi"}, args...)...)
+ cmd := operationCommand(args)
cmd.Stdout = io.MultiWriter(os.Stdout, &output)
cmd.Stderr = io.MultiWriter(os.Stderr, &output)
return tea.ExecProcess(cmd, func(e error) tea.Msg { return resultMsg{err: e, text: output.String()} })()
@@ -422,10 +422,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "l":
m.page = 5
m.detail = ""
- return m, task(func(ctx context.Context) (string, error) {
- b, e := exec.CommandContext(ctx, "/usr/bin/journalctl", "-u", "fdpi.service", "-n", "80", "--no-pager", "-o", "short").CombinedOutput()
- return string(b), e
- })
+ return m, task(recentLogs)
case "esc":
m.detail = ""
m.search = ""
@@ -471,7 +468,7 @@ func (m Model) contentView() string {
case 0:
b.WriteString(m.dashboard())
case 4:
- b.WriteString("inet fdpi · исходящий трафик · IPv4 + IPv6\nNFQUEUE 24000 · без движка трафик идёт напрямую\nTCP 80/443: первые 20 пакетов\nUDP 443: первые 12 пакетов при включённом QUIC\nДомен из Host/SNI проверяет nfqws2.\n\nДоменные профили не разделяют приложения.\nd — диагностика без изменения сети")
+ b.WriteString("inet fdpi · исходящий трафик · IPv4 + IPv6\n" + engineLabel() + " · без движка трафик идёт напрямую\nTCP 80/443: первые 20 пакетов\nUDP 443: первые 12 пакетов при включённом QUIC\nДомен из Host/SNI проверяет nfqws2.\n\nДоменные профили не разделяют приложения.\nd — диагностика без изменения сети")
case 5:
b.WriteString("l загрузить журнал · / поиск · j/k прокрутка\nДля чтения журнала могут потребоваться права.")
default:
diff --git a/scripts/build-engine-windows.sh b/scripts/build-engine-windows.sh
new file mode 100755
index 0000000..7236288
--- /dev/null
+++ b/scripts/build-engine-windows.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# Cross-build the FDPI engine (winws2.exe) for Windows from Linux.
+# winws2 is the WinDivert-based Windows frontend of zapret2's nfq2, built with
+# mingw-w64 instead of upstream's cygwin via scripts/windows/mingw-compat shims.
+set -euo pipefail
+cd -- "$(dirname -- "$0")/.."
+
+commit=9b9b929f837978173d787a5773a47a91d90f7f81
+MIRROR="$PWD/scripts/windows/mingw-compat"
+DEPS=/tmp/fdpi-win-deps
+
+CC=${CC:-x86_64-w64-mingw32-gcc}
+AR=${AR:-x86_64-w64-mingw32-ar}
+command -v "$CC" >/dev/null || { echo "Нужен mingw-w64 (x86_64-w64-mingw32-gcc)" >&2; exit 1; }
+command -v git >/dev/null || { echo "Нужен git" >&2; exit 1; }
+
+mkdir -p build "$DEPS"
+
+# --- zapret2 sources at the pinned commit ---
+if [[ ! -d build/zapret2/.git ]]; then
+ git clone --no-checkout https://github.com/bol-van/zapret2.git build/zapret2
+fi
+git -C build/zapret2 fetch origin "$commit"
+git -C build/zapret2 checkout --detach "$commit"
+
+# --- apply mingw patch + rename the remaining cygwin-only guards ---
+patched=build/zapret2/.fdpi-mingw-patched
+if [[ ! -f "$patched" ]]; then
+ git -C build/zapret2 apply "$PWD/scripts/windows/patches/zapret2-mingw.patch"
+ for f in build/zapret2/nfq2/*.c build/zapret2/nfq2/*.h build/zapret2/nfq2/crypto/*.c build/zapret2/nfq2/crypto/*.h; do
+ case "$f" in
+ */helpers.c|*/lua.c|*/sec.c) continue ;; # already renamed by the patch
+ esac
+ sed -i 's/__CYGWIN__/ZAPRET_WIN/g' "$f"
+ done
+ touch "$patched"
+fi
+
+# --- Lua 5.4 static library for mingw ---
+if [[ ! -f "$DEPS/liblua-mingw.a" ]]; then
+ if [[ ! -d "$DEPS/lua-5.4.8/src" ]]; then
+ curl -fsSL https://www.lua.org/ftp/lua-5.4.8.tar.gz | tar xz -C "$DEPS"
+ fi
+ for f in lapi lauxlib lbaselib lcode lcorolib lctype ldblib ldebug ldo ldump lfunc lgc \
+ linit liolib llex lmathlib lmem loadlib lobject lopcodes loslib lparser lstate \
+ lstring lstrlib ltable ltablib ltm lundump lutf8lib lvm lzio; do
+ "$CC" -O2 -c "$DEPS/lua-5.4.8/src/$f.c" -o "$DEPS/lua-5.4.8/src/$f.o"
+ done
+ "$AR" rcs "$DEPS/liblua-mingw.a" "$DEPS"/lua-5.4.8/src/l*.o
+fi
+
+# --- zlib static library for mingw ---
+if [[ ! -f "$DEPS/libz.a" ]]; then
+ if [[ ! -d "$DEPS/zlib-1.3.1" ]]; then
+ curl -fsSL https://zlib.net/fossils/zlib-1.3.1.tar.gz | tar xz -C "$DEPS"
+ fi
+ make -s -C "$DEPS/zlib-1.3.1" -f win32/Makefile.gcc PREFIX=${CC%-gcc}- libz.a
+ cp "$DEPS/zlib-1.3.1/libz.a" "$DEPS/libz.a"
+fi
+
+# --- compile winws2 ---
+NFQ=build/zapret2/nfq2
+CF="-std=gnu99 -O2 -DZAPRET_WIN -D_WIN32_WINNT=0x0600"
+CF+=" -include $MIRROR/include/zcompat_extra.h"
+CF+=" -I$MIRROR/include -I$NFQ/windows -I$DEPS/lua-5.4.8/src -I$DEPS/zlib-1.3.1"
+OBJ=build/windows-obj
+mkdir -p "$OBJ"
+srcs=("$NFQ"/*.c "$NFQ"/crypto/*.c "$MIRROR"/compat.c)
+for f in "${srcs[@]}"; do
+ o="$OBJ/$(echo "${f#$NFQ/}" | tr '/' '_').o"
+ "$CC" $CF -c "$f" -o "$o"
+done
+"$CC" -static -Wl,--gc-sections -o build/winws2.exe "$OBJ"/*.o \
+ "$DEPS/liblua-mingw.a" "$DEPS/libz.a" \
+ -L"$NFQ/windows/windivert" -lwindivert64 \
+ -lws2_32 -liphlpapi -lole32 -loleaut32 -luuid -lntdll -lwlanapi -ladvapi32 -lshlwapi \
+ -lwinpthread
+
+mkdir -p build/windows/lua
+cp build/winws2.exe build/windows/
+install -m644 build/zapret2/lua/zapret-lib.lua build/zapret2/lua/zapret-antidpi.lua build/windows/lua/
+install -m644 build/zapret2/docs/LICENSE.txt build/windows/ZAPRET-LICENSE
+printf '%s\n' "$commit" > build/windows/engine.commit
+cp third_party/windivert/x64/WinDivert.dll build/windows/
+if command -v wine >/dev/null; then
+ ( cd build/windows && wine ./winws2.exe --version ) || echo "Внимание: wine не смог запустить winws2.exe — проверка версии пропущена"
+fi
+echo "Движок собран: build/windows/winws2.exe"
diff --git a/scripts/windows/mingw-compat/compat.c b/scripts/windows/mingw-compat/compat.c
new file mode 100644
index 0000000..41f1832
--- /dev/null
+++ b/scripts/windows/mingw-compat/compat.c
@@ -0,0 +1,164 @@
+// mingw compatibility shims for winws2 (zapret2 nfq2 built with mingw instead of cygwin)
+#define _WIN32_WINNT 0x0600
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "include/sys/utsname.h"
+#include "include/ifaddrs.h"
+#include "include/wordexp.h"
+#include "include/sys/cygwin.h"
+#include "include/libgen.h"
+#include
+
+// ---- uname ----
+int uname(struct utsname *buf) {
+ OSVERSIONINFOEXA vi; DWORD sz = sizeof(buf->nodename);
+ memset(&vi, 0, sizeof(vi)); vi.dwOSVersionInfoSize = sizeof(vi);
+ GetVersionExA((OSVERSIONINFOA*)&vi);
+ strcpy(buf->sysname, "Windows");
+ GetComputerNameA(buf->nodename, &sz);
+ snprintf(buf->release, sizeof(buf->release), "%lu.%lu.%lu", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber);
+ strcpy(buf->version, "Windows NT");
+#if defined(_M_X64) || defined(__x86_64__)
+ strcpy(buf->machine, "x86_64");
+#else
+ strcpy(buf->machine, "i686");
+#endif
+ return 0;
+}
+
+// ---- getifaddrs over GetAdaptersAddresses ----
+int getifaddrs(struct ifaddrs **ifap) {
+ ULONG flags = GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER;
+ ULONG buflen = 16384;
+ IP_ADAPTER_ADDRESSES *aa = malloc(buflen);
+ struct ifaddrs *head = NULL, *tail = NULL;
+ *ifap = NULL;
+ if (!aa) return -1;
+ if (GetAdaptersAddresses(AF_UNSPEC, flags, NULL, aa, &buflen) != ERROR_SUCCESS) { free(aa); return -1; }
+ for (IP_ADAPTER_ADDRESSES *a = aa; a; a = a->Next) {
+ for (IP_ADAPTER_UNICAST_ADDRESS *u = a->FirstUnicastAddress; u; u = u->Next) {
+ struct ifaddrs *ifa = calloc(1, sizeof(*ifa));
+ if (!ifa) break;
+ ifa->ifa_name = _strdup(a->AdapterName);
+ ifa->ifa_flags = IFF_UP;
+ if (!(a->OperStatus & IfOperStatusUp)) ifa->ifa_flags &= ~IFF_UP;
+ if (a->IfType == IF_TYPE_SOFTWARE_LOOPBACK) ifa->ifa_flags |= IFF_LOOPBACK;
+ size_t salen = u->Address.lpSockaddr->sa_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
+ ifa->ifa_addr = malloc(salen); memcpy(ifa->ifa_addr, u->Address.lpSockaddr, salen);
+ if (u->Address.lpSockaddr->sa_family == AF_INET) {
+ struct sockaddr_in *nm = calloc(1, sizeof(struct sockaddr_in));
+ nm->sin_family = AF_INET;
+ ULONG plen = u->OnLinkPrefixLength;
+ ULONG mask = plen ? (0xFFFFFFFFu << (32 - plen)) : 0;
+ nm->sin_addr.s_addr = htonl(mask);
+ ifa->ifa_netmask = (struct sockaddr*)nm;
+ } else {
+ struct sockaddr_in6 *nm = calloc(1, sizeof(struct sockaddr_in6));
+ nm->sin6_family = AF_INET6;
+ ULONG plen = u->OnLinkPrefixLength;
+ for (ULONG i = 0; i < 16; i++) {
+ ULONG bits = plen > i * 8 ? (plen - i * 8) : 0;
+ nm->sin6_addr.s6_addr[i] = bits >= 8 ? 0xFF : (bits ? (0xFF << (8 - bits)) : 0);
+ }
+ ifa->ifa_netmask = (struct sockaddr*)nm;
+ }
+ ifa->ifa_broadaddr = NULL; ifa->ifa_dstaddr = NULL;
+ if (!head) head = tail = ifa; else { tail->ifa_next = ifa; tail = ifa; }
+ }
+ }
+ free(aa);
+ *ifap = head;
+ return 0;
+}
+void freeifaddrs(struct ifaddrs *ifa) {
+ while (ifa) {
+ struct ifaddrs *n = ifa->ifa_next;
+ free(ifa->ifa_name); free(ifa->ifa_addr); free(ifa->ifa_netmask); free(ifa);
+ ifa = n;
+ }
+}
+
+// ---- wordexp: simple whitespace tokenizer (no expansion; FDPI does not use @config files) ----
+int wordexp(const char *s, wordexp_t *p, int flags) {
+ (void)flags;
+ char *copy = _strdup(s ? s : "");
+ if (!copy) return WRDE_NOSPACE;
+ size_t cap = 16, n = 0;
+ char **v = malloc(cap * sizeof(char*));
+ if (!v) { free(copy); return WRDE_NOSPACE; }
+ char *save = NULL;
+ for (char *t = strtok_r(copy, " \t\r\n", &save); t; t = strtok_r(NULL, " \t\r\n", &save)) {
+ if (n == cap) { cap *= 2; char **nv = realloc(v, cap * sizeof(char*)); if (!nv) { free(v); free(copy); return WRDE_NOSPACE; } v = nv; }
+ v[n++] = _strdup(t);
+ }
+ p->we_wordc = n; p->we_wordv = v; p->we_offs = 0;
+ free(copy);
+ return 0;
+}
+void wordfree(wordexp_t *p) {
+ for (size_t i = 0; i < p->we_wordc; i++) free(p->we_wordv[i]);
+ free(p->we_wordv); p->we_wordc = 0; p->we_wordv = NULL;
+}
+
+// ---- cygwin_conv_path ----
+/* Contract (matches cygwin): with to==NULL returns required buffer size;
+ with to!=NULL writes the converted path and returns 0 on success, -1 on error. */
+size_t cygwin_conv_path(unsigned what, const void *from, void *to, size_t size) {
+ if (!from) return (size_t)-1;
+ if (what & CCP_POSIX_TO_WIN_W) {
+ int l = MultiByteToWideChar(CP_UTF8, 0, (const char*)from, -1, NULL, 0);
+ if (!to) return (size_t)l * 2;
+ if (size < (size_t)l * 2) return (size_t)-1;
+ MultiByteToWideChar(CP_UTF8, 0, (const char*)from, -1, (wchar_t*)to, l);
+ return 0;
+ }
+ {
+ const char *s = (const char*)from;
+ static char buf[4096];
+ if (what & CCP_WIN_W_TO_POSIX) {
+ WideCharToMultiByte(CP_UTF8, 0, (const wchar_t*)from, -1, buf, sizeof(buf), NULL, NULL);
+ s = buf;
+ }
+ size_t l = strlen(s) + 1;
+ if (!to) return l;
+ if (size < l) return (size_t)-1;
+ memcpy(to, s, l);
+ return 0;
+ }
+}
+
+// ---- libgen ----
+static char *g_base, *g_dir;
+char *basename(char *path) {
+ char *p = strrchr(path, '\\'); char *q = strrchr(path, '/');
+ char *sep = p > q ? p : q;
+ free(g_base);
+ g_base = _strdup(sep ? sep + 1 : path);
+ return g_base;
+}
+char *dirname(char *path) {
+ char tmp[4096]; strncpy(tmp, path, sizeof(tmp) - 1); tmp[sizeof(tmp)-1] = 0;
+ char *p = strrchr(tmp, '\\'); char *q = strrchr(tmp, '/');
+ char *sep = p > q ? p : q;
+ if (sep) *sep = 0; else strcpy(tmp, ".");
+ free(g_dir); g_dir = _strdup(tmp);
+ return g_dir;
+}
+
+// ---- getrandom ----
+int getrandom(void *buf, size_t buflen, unsigned int flags) {
+ (void)flags;
+ unsigned char *p = buf;
+ while (buflen) {
+ unsigned int r = rand();
+ size_t n = buflen < sizeof(r) ? buflen : sizeof(r);
+ memcpy(p, &r, n);
+ p += n; buflen -= n;
+ }
+ return 0;
+}
diff --git a/scripts/windows/mingw-compat/include/KnownFolders.h b/scripts/windows/mingw-compat/include/KnownFolders.h
new file mode 100644
index 0000000..4b26109
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/KnownFolders.h
@@ -0,0 +1,2 @@
+#pragma once
+#include
diff --git a/scripts/windows/mingw-compat/include/arpa/inet.h b/scripts/windows/mingw-compat/include/arpa/inet.h
new file mode 100644
index 0000000..1a248d5
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/arpa/inet.h
@@ -0,0 +1,6 @@
+#pragma once
+#ifndef _WINSOCK2API_
+#define WIN32_LEAN_AND_MEAN 1
+#include
+#include
+#endif
diff --git a/scripts/windows/mingw-compat/include/grp.h b/scripts/windows/mingw-compat/include/grp.h
new file mode 100644
index 0000000..d6d29c9
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/grp.h
@@ -0,0 +1,4 @@
+#pragma once
+struct group { char *gr_name; char *gr_passwd; int gr_gid; char **gr_mem; };
+static struct group *getgrnam(const char *name) { (void)name; return (void*)0; }
+static struct group *getgrgid(int gid) { (void)gid; return (void*)0; }
diff --git a/scripts/windows/mingw-compat/include/ifaddrs.h b/scripts/windows/mingw-compat/include/ifaddrs.h
new file mode 100644
index 0000000..483bcea
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/ifaddrs.h
@@ -0,0 +1,10 @@
+#pragma once
+struct ifaddrs { struct ifaddrs *ifa_next; char *ifa_name; unsigned int ifa_flags; struct sockaddr *ifa_addr; struct sockaddr *ifa_netmask; struct ifaddrs *__ifa_pad1; struct sockaddr *ifa_broadaddr; struct sockaddr *ifa_dstaddr; void *__ifa_pad2; };
+#ifdef __cplusplus
+extern "C" {
+#endif
+int getifaddrs(struct ifaddrs **ifap);
+void freeifaddrs(struct ifaddrs *ifa);
+#ifdef __cplusplus
+}
+#endif
diff --git a/scripts/windows/mingw-compat/include/knownfolders.h b/scripts/windows/mingw-compat/include/knownfolders.h
new file mode 100644
index 0000000..4b26109
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/knownfolders.h
@@ -0,0 +1,2 @@
+#pragma once
+#include
diff --git a/scripts/windows/mingw-compat/include/libgen.h b/scripts/windows/mingw-compat/include/libgen.h
new file mode 100644
index 0000000..4a55b5f
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/libgen.h
@@ -0,0 +1,9 @@
+#pragma once
+#ifdef __cplusplus
+extern "C" {
+#endif
+char *basename(char *path);
+char *dirname(char *path);
+#ifdef __cplusplus
+}
+#endif
diff --git a/scripts/windows/mingw-compat/include/net/if.h b/scripts/windows/mingw-compat/include/net/if.h
new file mode 100644
index 0000000..0f62507
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/net/if.h
@@ -0,0 +1,10 @@
+#pragma once
+#ifndef IFNAMSIZ
+#define IFNAMSIZ 256
+#endif
+#ifndef _WINSOCK2API_
+#define WIN32_LEAN_AND_MEAN 1
+#include
+#include
+#endif
+#include
diff --git a/scripts/windows/mingw-compat/include/netinet/in.h b/scripts/windows/mingw-compat/include/netinet/in.h
new file mode 100644
index 0000000..de7984f
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/netinet/in.h
@@ -0,0 +1,9 @@
+#pragma once
+#ifndef _WINSOCK2API_
+#define WIN32_LEAN_AND_MEAN 1
+#include
+#include
+#endif
+#define IPPROTO_IGMP 2
+#define IPPROTO_IPIP 4
+#define IPPROTO_IPV6 41
diff --git a/scripts/windows/mingw-compat/include/netinet/ip.h b/scripts/windows/mingw-compat/include/netinet/ip.h
new file mode 100644
index 0000000..f6b7bca
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/netinet/ip.h
@@ -0,0 +1,23 @@
+#pragma once
+#include
+#include
+struct ip {
+#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ uint8_t ip_hl:4, ip_v:4;
+#else
+ uint8_t ip_v:4, ip_hl:4;
+#endif
+ uint8_t ip_tos;
+ uint16_t ip_len, ip_id, ip_off;
+ uint8_t ip_ttl, ip_p;
+ uint16_t ip_sum;
+ struct in_addr ip_src, ip_dst;
+};
+#define IP_RF 0x8000
+#define IP_DF 0x4000
+#define IP_MF 0x2000
+#define IP_OFFMASK 0x1fff
+#define IP_HDRLEN(ip) ((ip)->ip_hl * 4)
+#ifndef IPVERSION
+#define IPVERSION 4
+#endif
diff --git a/scripts/windows/mingw-compat/include/netinet/ip6.h b/scripts/windows/mingw-compat/include/netinet/ip6.h
new file mode 100644
index 0000000..fa1534d
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/netinet/ip6.h
@@ -0,0 +1,31 @@
+#pragma once
+#include
+#include
+struct ip6_hdr {
+ union {
+ struct {
+ uint32_t ip6_un1_flow;
+ uint16_t ip6_un1_plen;
+ uint8_t ip6_un1_nxt;
+ uint8_t ip6_un1_hlim;
+ } ip6_un1;
+ uint8_t ip6_un2_vfc;
+ } ip6_ctlun;
+ struct in6_addr ip6_src;
+ struct in6_addr ip6_dst;
+};
+#define ip6_vfc ip6_ctlun.ip6_un2_vfc
+#define ip6_flow ip6_ctlun.ip6_un1.ip6_un1_flow
+#define ip6_plen ip6_ctlun.ip6_un1.ip6_un1_plen
+#define ip6_nxt ip6_ctlun.ip6_un1.ip6_un1_nxt
+#define ip6_hlim ip6_ctlun.ip6_un1.ip6_un1_hlim
+#define ip6_hops ip6_ctlun.ip6_un1.ip6_un1_hlim
+#define IP6_VERSION 6
+struct ip6_frag {
+ uint8_t ip6f_nxt;
+ uint8_t ip6f_reserved;
+ uint16_t ip6f_offlg;
+ uint32_t ip6f_ident;
+};
+#define IP6F_OFF_MASK 0xfff8
+#define IP6F_MORE_FRAG 0x0001
diff --git a/scripts/windows/mingw-compat/include/netinet/tcp.h b/scripts/windows/mingw-compat/include/netinet/tcp.h
new file mode 100644
index 0000000..0e586c8
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/netinet/tcp.h
@@ -0,0 +1,9 @@
+#pragma once
+#include
+struct tcphdr { uint16_t th_sport, th_dport; uint32_t th_seq, th_ack; uint8_t th_x2:4, th_off:4; uint8_t th_flags; uint16_t th_win, th_sum, th_urp; };
+#define TH_FIN 0x01
+#define TH_SYN 0x02
+#define TH_RST 0x04
+#define TH_PUSH 0x08
+#define TH_ACK 0x10
+#define TH_URG 0x20
diff --git a/scripts/windows/mingw-compat/include/netinet/udp.h b/scripts/windows/mingw-compat/include/netinet/udp.h
new file mode 100644
index 0000000..cd327cb
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/netinet/udp.h
@@ -0,0 +1,3 @@
+#pragma once
+#include
+struct udphdr { uint16_t uh_sport, uh_dport, uh_ulen, uh_sum; };
diff --git a/scripts/windows/mingw-compat/include/pwd.h b/scripts/windows/mingw-compat/include/pwd.h
new file mode 100644
index 0000000..3e04099
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/pwd.h
@@ -0,0 +1,4 @@
+#pragma once
+struct passwd { char *pw_name; char *pw_passwd; int pw_uid; int pw_gid; char *pw_gecos; char *pw_dir; char *pw_shell; };
+static struct passwd *getpwnam(const char *name) { (void)name; return (void*)0; }
+static struct passwd *getpwuid(int uid) { (void)uid; return (void*)0; }
diff --git a/scripts/windows/mingw-compat/include/sys/cygwin.h b/scripts/windows/mingw-compat/include/sys/cygwin.h
new file mode 100644
index 0000000..9a72ec2
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/sys/cygwin.h
@@ -0,0 +1,13 @@
+#pragma once
+#include
+#define CCP_POSIX_TO_WIN_W 0x100000
+#define CCP_WIN_W_TO_POSIX 0x80000
+#define CCP_WIN_A_TO_POSIX 0x40000
+#define CCP_ABSOLUTE 0x1000
+#ifdef __cplusplus
+extern "C" {
+#endif
+size_t cygwin_conv_path(unsigned what, const void *from, void *to, size_t size);
+#ifdef __cplusplus
+}
+#endif
diff --git a/scripts/windows/mingw-compat/include/sys/ioctl.h b/scripts/windows/mingw-compat/include/sys/ioctl.h
new file mode 100644
index 0000000..2f4b733
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/sys/ioctl.h
@@ -0,0 +1,5 @@
+#pragma once
+#ifndef FIONREAD
+#define FIONREAD 0x4004667f
+#endif
+static int ioctl(int fd, unsigned long req, ...) { (void)fd; (void)req; return -1; }
diff --git a/scripts/windows/mingw-compat/include/sys/queue.h b/scripts/windows/mingw-compat/include/sys/queue.h
new file mode 100644
index 0000000..e2603bc
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/sys/queue.h
@@ -0,0 +1,1097 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1991, 1993
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef _SYS_QUEUE_H_
+#define _SYS_QUEUE_H_
+
+#include
+
+/*
+ * This file defines four types of data structures: singly-linked lists,
+ * singly-linked tail queues, lists and tail queues.
+ *
+ * A singly-linked list is headed by a single forward pointer. The elements
+ * are singly linked for minimum space and pointer manipulation overhead at
+ * the expense of O(n) removal for arbitrary elements. New elements can be
+ * added to the list after an existing element or at the head of the list.
+ * Elements being removed from the head of the list should use the explicit
+ * macro for this purpose for optimum efficiency. A singly-linked list may
+ * only be traversed in the forward direction. Singly-linked lists are ideal
+ * for applications with large datasets and few or no removals or for
+ * implementing a LIFO queue.
+ *
+ * A singly-linked tail queue is headed by a pair of pointers, one to the
+ * head of the list and the other to the tail of the list. The elements are
+ * singly linked for minimum space and pointer manipulation overhead at the
+ * expense of O(n) removal for arbitrary elements. New elements can be added
+ * to the list after an existing element, at the head of the list, or at the
+ * end of the list. Elements being removed from the head of the tail queue
+ * should use the explicit macro for this purpose for optimum efficiency.
+ * A singly-linked tail queue may only be traversed in the forward direction.
+ * Singly-linked tail queues are ideal for applications with large datasets
+ * and few or no removals or for implementing a FIFO queue.
+ *
+ * A list is headed by a single forward pointer (or an array of forward
+ * pointers for a hash table header). The elements are doubly linked
+ * so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before
+ * or after an existing element or at the head of the list. A list
+ * may be traversed in either direction.
+ *
+ * A tail queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are doubly
+ * linked so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before or
+ * after an existing element, at the head of the list, or at the end of
+ * the list. A tail queue may be traversed in either direction.
+ *
+ * For details on the use of these macros, see the queue(3) manual page.
+ *
+ * Below is a summary of implemented functions where:
+ * + means the macro is available
+ * - means the macro is not available
+ * s means the macro is available but is slow (runs in O(n) time)
+ *
+ * SLIST LIST STAILQ TAILQ
+ * _HEAD + + + +
+ * _CLASS_HEAD + + + +
+ * _HEAD_INITIALIZER + + + +
+ * _ENTRY + + + +
+ * _CLASS_ENTRY + + + +
+ * _INIT + + + +
+ * _EMPTY + + + +
+ * _END + + + +
+ * _FIRST + + + +
+ * _NEXT + + + +
+ * _PREV - + - +
+ * _LAST - - + +
+ * _LAST_FAST - - - +
+ * _FOREACH + + + +
+ * _FOREACH_FROM + + + +
+ * _FOREACH_SAFE + + + +
+ * _FOREACH_FROM_SAFE + + + +
+ * _FOREACH_REVERSE - - - +
+ * _FOREACH_REVERSE_FROM - - - +
+ * _FOREACH_REVERSE_SAFE - - - +
+ * _FOREACH_REVERSE_FROM_SAFE - - - +
+ * _INSERT_HEAD + + + +
+ * _INSERT_BEFORE - + - +
+ * _INSERT_AFTER + + + +
+ * _INSERT_TAIL - - + +
+ * _CONCAT s s + +
+ * _REMOVE_AFTER + - + -
+ * _REMOVE_HEAD + + + +
+ * _REMOVE s + s +
+ * _REPLACE - + - +
+ * _SPLIT_AFTER + + + +
+ * _SWAP + + + +
+ *
+ */
+#ifdef QUEUE_MACRO_DEBUG
+#warn Use QUEUE_MACRO_DEBUG_xxx instead (TRACE, TRASH and/or ASSERTIONS)
+#define QUEUE_MACRO_DEBUG_TRACE
+#define QUEUE_MACRO_DEBUG_TRASH
+#endif
+#ifdef QUEUE_MACRO_DEBUG_TRACE
+/* Store the last 2 places the queue element or head was altered */
+struct qm_trace {
+ unsigned long lastline;
+ unsigned long prevline;
+ const char *lastfile;
+ const char *prevfile;
+};
+
+#define TRACEBUF struct qm_trace trace;
+#define TRACEBUF_INITIALIZER { __LINE__, 0, __FILE__, NULL } ,
+
+#define QMD_TRACE_HEAD(head) do { \
+ (head)->trace.prevline = (head)->trace.lastline; \
+ (head)->trace.prevfile = (head)->trace.lastfile; \
+ (head)->trace.lastline = __LINE__; \
+ (head)->trace.lastfile = __FILE__; \
+} while (0)
+
+#define QMD_TRACE_ELEM(elem) do { \
+ (elem)->trace.prevline = (elem)->trace.lastline; \
+ (elem)->trace.prevfile = (elem)->trace.lastfile; \
+ (elem)->trace.lastline = __LINE__; \
+ (elem)->trace.lastfile = __FILE__; \
+} while (0)
+
+#else /* !QUEUE_MACRO_DEBUG_TRACE */
+#define QMD_TRACE_ELEM(elem)
+#define QMD_TRACE_HEAD(head)
+#define TRACEBUF
+#define TRACEBUF_INITIALIZER
+#endif /* QUEUE_MACRO_DEBUG_TRACE */
+
+#ifdef QUEUE_MACRO_DEBUG_TRASH
+#define QMD_SAVELINK(name, link) void **name = (void *)&(link)
+#define TRASHIT(x) do {(x) = (void *)-1;} while (0)
+#define QMD_IS_TRASHED(x) ((x) == (void *)(intptr_t)-1)
+#else /* !QUEUE_MACRO_DEBUG_TRASH */
+#define QMD_SAVELINK(name, link)
+#define TRASHIT(x)
+#define QMD_IS_TRASHED(x) 0
+#endif /* QUEUE_MACRO_DEBUG_TRASH */
+
+#if defined(QUEUE_MACRO_DEBUG_ASSERTIONS) && \
+ defined(QUEUE_MACRO_NO_DEBUG_ASSERTIONS)
+#error Both QUEUE_MACRO_DEBUG_ASSERTIONS and QUEUE_MACRO_NO_DEBUG_ASSERTIONS defined
+#endif
+
+/*
+ * Automatically define QUEUE_MACRO_DEBUG_ASSERTIONS when compiling the kernel
+ * with INVARIANTS, if not already defined and not prevented by presence of
+ * QUEUE_MACRO_NO_DEBUG_ASSERTIONS.
+ */
+#if !defined(QUEUE_MACRO_DEBUG_ASSERTIONS) && \
+ !defined(QUEUE_MACRO_NO_DEBUG_ASSERTIONS) && \
+ (defined(_KERNEL) && defined(INVARIANTS))
+#define QUEUE_MACRO_DEBUG_ASSERTIONS
+#endif
+
+/*
+ * If queue assertions are enabled, provide default definitions for QMD_PANIC()
+ * and QMD_ASSERT() if undefined.
+ */
+#ifdef QUEUE_MACRO_DEBUG_ASSERTIONS
+#ifndef QMD_PANIC
+#if defined(_KERNEL) || defined(_STANDALONE)
+/*
+ * On _STANDALONE, either or the headers using provide
+ * a declaration or macro for panic().
+ */
+#ifdef _KERNEL
+#include
+#endif
+#define QMD_PANIC(fmt, ...) do { \
+ panic(fmt, ##__VA_ARGS__); \
+} while (0)
+#else /* !(_KERNEL || _STANDALONE) */
+#include
+#include
+#define QMD_PANIC(fmt, ...) do { \
+ fprintf(stderr, fmt "\n", ##__VA_ARGS__); \
+ abort(); \
+} while (0)
+#endif /* _KERNEL || _STANDALONE */
+#endif /* !QMD_PANIC */
+
+#ifndef QMD_ASSERT
+#define QMD_ASSERT(expression, fmt, ...) do { \
+ if (__predict_false(!(expression))) \
+ QMD_PANIC("%s:%u: %s: " fmt, \
+ __FILE__, __LINE__, __func__, ##__VA_ARGS__); \
+} while (0)
+#endif /* !QMD_ASSERT */
+#else /* !QUEUE_MACRO_DEBUG_ASSERTIONS */
+#undef QMD_ASSERT
+#define QMD_ASSERT(test, fmt, ...) do {} while (0)
+#endif /* QUEUE_MACRO_DEBUG_ASSERTIONS */
+
+
+#ifdef __cplusplus
+/*
+ * In C++ there can be structure lists and class lists:
+ */
+#define QUEUE_TYPEOF(type) type
+#else
+#define QUEUE_TYPEOF(type) struct type
+#endif
+
+/*
+ * Singly-linked List declarations.
+ */
+
+#define SLIST_HEAD(name, type) \
+struct name { \
+ struct type *slh_first; /* first element */ \
+}
+
+#define SLIST_CLASS_HEAD(name, type) \
+struct name { \
+ class type *slh_first; /* first element */ \
+}
+
+#define SLIST_HEAD_INITIALIZER(head) \
+ { NULL }
+
+#define SLIST_ENTRY(type) \
+struct { \
+ struct type *sle_next; /* next element */ \
+}
+
+#define SLIST_CLASS_ENTRY(type) \
+struct { \
+ class type *sle_next; /* next element */ \
+}
+
+/*
+ * Singly-linked List functions.
+ */
+
+#define QMD_SLIST_CHECK_PREVPTR(prevp, elm) \
+ QMD_ASSERT(*(prevp) == (elm), \
+ "Bad prevptr *(%p) == %p != %p", \
+ (prevp), *(prevp), (elm))
+
+#define SLIST_ASSERT_EMPTY(head) \
+ QMD_ASSERT(SLIST_EMPTY((head)), \
+ "slist %p is not empty", (head))
+
+#define SLIST_ASSERT_NONEMPTY(head) \
+ QMD_ASSERT(!SLIST_EMPTY((head)), \
+ "slist %p is empty", (head))
+
+#define SLIST_CONCAT(head1, head2, type, field) do { \
+ QUEUE_TYPEOF(type) *_Curelm = SLIST_FIRST(head1); \
+ if (_Curelm == NULL) { \
+ if ((SLIST_FIRST(head1) = SLIST_FIRST(head2)) != NULL) \
+ SLIST_INIT(head2); \
+ } else if (SLIST_FIRST(head2) != NULL) { \
+ while (SLIST_NEXT(_Curelm, field) != NULL) \
+ _Curelm = SLIST_NEXT(_Curelm, field); \
+ SLIST_NEXT(_Curelm, field) = SLIST_FIRST(head2); \
+ SLIST_INIT(head2); \
+ } \
+} while (0)
+
+#define SLIST_EMPTY(head) ((head)->slh_first == NULL)
+
+#define SLIST_EMPTY_ATOMIC(head) \
+ (atomic_load_ptr(&(head)->slh_first) == NULL)
+
+#define SLIST_FIRST(head) ((head)->slh_first)
+
+#define SLIST_FOREACH(var, head, field) \
+ for ((var) = SLIST_FIRST((head)); \
+ (var); \
+ (var) = SLIST_NEXT((var), field))
+
+#define SLIST_FOREACH_FROM(var, head, field) \
+ for ((var) = ((var) ? (var) : SLIST_FIRST((head))); \
+ (var); \
+ (var) = SLIST_NEXT((var), field))
+
+#define SLIST_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = SLIST_FIRST((head)); \
+ (var) && ((tvar) = SLIST_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define SLIST_FOREACH_FROM_SAFE(var, head, field, tvar) \
+ for ((var) = ((var) ? (var) : SLIST_FIRST((head))); \
+ (var) && ((tvar) = SLIST_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define SLIST_FOREACH_PREVPTR(var, varp, head, field) \
+ for ((varp) = &SLIST_FIRST((head)); \
+ ((var) = *(varp)) != NULL; \
+ (varp) = &SLIST_NEXT((var), field))
+
+#define SLIST_INIT(head) do { \
+ SLIST_FIRST((head)) = NULL; \
+} while (0)
+
+#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \
+ SLIST_NEXT((elm), field) = SLIST_NEXT((slistelm), field); \
+ SLIST_NEXT((slistelm), field) = (elm); \
+} while (0)
+
+#define SLIST_INSERT_HEAD(head, elm, field) do { \
+ SLIST_NEXT((elm), field) = SLIST_FIRST((head)); \
+ SLIST_FIRST((head)) = (elm); \
+} while (0)
+
+#define SLIST_NEXT(elm, field) ((elm)->field.sle_next)
+
+#define SLIST_REMOVE(head, elm, type, field) do { \
+ if (SLIST_FIRST((head)) == (elm)) { \
+ SLIST_REMOVE_HEAD((head), field); \
+ } \
+ else { \
+ QUEUE_TYPEOF(type) *_Curelm = SLIST_FIRST(head); \
+ while (SLIST_NEXT(_Curelm, field) != (elm)) \
+ _Curelm = SLIST_NEXT(_Curelm, field); \
+ SLIST_REMOVE_AFTER(_Curelm, field); \
+ } \
+} while (0)
+
+#define SLIST_REMOVE_AFTER(elm, field) do { \
+ QMD_SAVELINK(_Oldnext, SLIST_NEXT(elm, field)->field.sle_next); \
+ SLIST_NEXT(elm, field) = \
+ SLIST_NEXT(SLIST_NEXT(elm, field), field); \
+ TRASHIT(*_Oldnext); \
+} while (0)
+
+#define SLIST_REMOVE_HEAD(head, field) do { \
+ QMD_SAVELINK(_Oldnext, SLIST_FIRST(head)->field.sle_next); \
+ SLIST_FIRST((head)) = SLIST_NEXT(SLIST_FIRST((head)), field); \
+ TRASHIT(*_Oldnext); \
+} while (0)
+
+#define SLIST_REMOVE_PREVPTR(prevp, elm, field) do { \
+ QMD_SLIST_CHECK_PREVPTR(prevp, elm); \
+ *(prevp) = SLIST_NEXT(elm, field); \
+ TRASHIT((elm)->field.sle_next); \
+} while (0)
+
+#define SLIST_SPLIT_AFTER(head, elm, rest, field) do { \
+ SLIST_ASSERT_NONEMPTY((head)); \
+ SLIST_FIRST((rest)) = SLIST_NEXT((elm), field); \
+ SLIST_NEXT((elm), field) = NULL; \
+} while (0)
+
+#define SLIST_SWAP(head1, head2, type) do { \
+ QUEUE_TYPEOF(type) *_Swap_first = SLIST_FIRST(head1); \
+ SLIST_FIRST(head1) = SLIST_FIRST(head2); \
+ SLIST_FIRST(head2) = _Swap_first; \
+} while (0)
+
+#define SLIST_END(head) NULL
+
+/*
+ * Singly-linked Tail queue declarations.
+ */
+
+#define STAILQ_HEAD(name, type) \
+struct name { \
+ struct type *stqh_first;/* first element */ \
+ struct type **stqh_last;/* addr of last next element */ \
+}
+
+#define STAILQ_CLASS_HEAD(name, type) \
+struct name { \
+ class type *stqh_first; /* first element */ \
+ class type **stqh_last; /* addr of last next element */ \
+}
+
+#define STAILQ_HEAD_INITIALIZER(head) \
+ { NULL, &(head).stqh_first }
+
+#define STAILQ_ENTRY(type) \
+struct { \
+ struct type *stqe_next; /* next element */ \
+}
+
+#define STAILQ_CLASS_ENTRY(type) \
+struct { \
+ class type *stqe_next; /* next element */ \
+}
+
+/*
+ * Singly-linked Tail queue functions.
+ */
+
+/*
+ * QMD_STAILQ_CHECK_EMPTY(STAILQ_HEAD *head)
+ *
+ * Validates that the stailq head's pointer to the last element's next pointer
+ * actually points to the head's first element pointer field.
+ */
+#define QMD_STAILQ_CHECK_EMPTY(head) \
+ QMD_ASSERT((head)->stqh_last == &(head)->stqh_first, \
+ "Empty stailq %p->stqh_last is %p, " \
+ "not head's first field address", \
+ (head), (head)->stqh_last)
+
+/*
+ * QMD_STAILQ_CHECK_TAIL(STAILQ_HEAD *head)
+ *
+ * Validates that the stailq's last element's next pointer is NULL.
+ */
+#define QMD_STAILQ_CHECK_TAIL(head) \
+ QMD_ASSERT(*(head)->stqh_last == NULL, \
+ "Stailq %p last element's next pointer is " \
+ "%p, not NULL", (head), *(head)->stqh_last)
+
+#define STAILQ_ASSERT_EMPTY(head) \
+ QMD_ASSERT(STAILQ_EMPTY((head)), \
+ "stailq %p is not empty", (head))
+
+#define STAILQ_ASSERT_NONEMPTY(head) \
+ QMD_ASSERT(!STAILQ_EMPTY((head)), \
+ "stailq %p is empty", (head))
+
+#define STAILQ_CONCAT(head1, head2) do { \
+ if (!STAILQ_EMPTY((head2))) { \
+ *(head1)->stqh_last = (head2)->stqh_first; \
+ (head1)->stqh_last = (head2)->stqh_last; \
+ STAILQ_INIT((head2)); \
+ } \
+} while (0)
+
+#define STAILQ_EMPTY(head) ({ \
+ if (STAILQ_FIRST(head) == NULL) \
+ QMD_STAILQ_CHECK_EMPTY(head); \
+ STAILQ_FIRST(head) == NULL; \
+})
+
+#define STAILQ_EMPTY_ATOMIC(head) \
+ (atomic_load_ptr(&(head)->stqh_first) == NULL)
+
+#define STAILQ_FIRST(head) ((head)->stqh_first)
+
+#define STAILQ_FOREACH(var, head, field) \
+ for((var) = STAILQ_FIRST((head)); \
+ (var); \
+ (var) = STAILQ_NEXT((var), field))
+
+#define STAILQ_FOREACH_FROM(var, head, field) \
+ for ((var) = ((var) ? (var) : STAILQ_FIRST((head))); \
+ (var); \
+ (var) = STAILQ_NEXT((var), field))
+
+#define STAILQ_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = STAILQ_FIRST((head)); \
+ (var) && ((tvar) = STAILQ_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define STAILQ_FOREACH_FROM_SAFE(var, head, field, tvar) \
+ for ((var) = ((var) ? (var) : STAILQ_FIRST((head))); \
+ (var) && ((tvar) = STAILQ_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define STAILQ_INIT(head) do { \
+ STAILQ_FIRST((head)) = NULL; \
+ (head)->stqh_last = &STAILQ_FIRST((head)); \
+} while (0)
+
+#define STAILQ_INSERT_AFTER(head, tqelm, elm, field) do { \
+ if ((STAILQ_NEXT((elm), field) = STAILQ_NEXT((tqelm), field)) == NULL)\
+ (head)->stqh_last = &STAILQ_NEXT((elm), field); \
+ STAILQ_NEXT((tqelm), field) = (elm); \
+} while (0)
+
+#define STAILQ_INSERT_HEAD(head, elm, field) do { \
+ if ((STAILQ_NEXT((elm), field) = STAILQ_FIRST((head))) == NULL) \
+ (head)->stqh_last = &STAILQ_NEXT((elm), field); \
+ STAILQ_FIRST((head)) = (elm); \
+} while (0)
+
+#define STAILQ_INSERT_TAIL(head, elm, field) do { \
+ QMD_STAILQ_CHECK_TAIL(head); \
+ __typeof__((head)->stqh_last) prevlast = (head)->stqh_last; \
+ STAILQ_NEXT((elm), field) = NULL; \
+ (head)->stqh_last = &STAILQ_NEXT((elm), field); \
+ *prevlast = (elm); \
+} while (0)
+
+#define STAILQ_LAST(head, type, field) \
+ (STAILQ_EMPTY((head)) ? NULL : \
+ __containerof((head)->stqh_last, \
+ QUEUE_TYPEOF(type), field.stqe_next))
+
+#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next)
+
+#define STAILQ_REMOVE(head, elm, type, field) do { \
+ QMD_SAVELINK(_Oldnext, (elm)->field.stqe_next); \
+ if (STAILQ_FIRST((head)) == (elm)) { \
+ STAILQ_REMOVE_HEAD((head), field); \
+ } \
+ else { \
+ QUEUE_TYPEOF(type) *_Curelm = STAILQ_FIRST(head); \
+ while (STAILQ_NEXT(_Curelm, field) != (elm)) \
+ _Curelm = STAILQ_NEXT(_Curelm, field); \
+ STAILQ_REMOVE_AFTER(head, _Curelm, field); \
+ } \
+ TRASHIT(*_Oldnext); \
+} while (0)
+
+#define STAILQ_REMOVE_AFTER(head, elm, field) do { \
+ if ((STAILQ_NEXT(elm, field) = \
+ STAILQ_NEXT(STAILQ_NEXT(elm, field), field)) == NULL) \
+ (head)->stqh_last = &STAILQ_NEXT((elm), field); \
+} while (0)
+
+#define STAILQ_REMOVE_HEAD(head, field) do { \
+ if ((STAILQ_FIRST((head)) = \
+ STAILQ_NEXT(STAILQ_FIRST((head)), field)) == NULL) \
+ (head)->stqh_last = &STAILQ_FIRST((head)); \
+} while (0)
+
+#define STAILQ_SPLIT_AFTER(head, elm, rest, field) do { \
+ STAILQ_ASSERT_NONEMPTY((head)); \
+ QMD_STAILQ_CHECK_TAIL((head)); \
+ if (STAILQ_NEXT((elm), field) == NULL) \
+ /* 'elm' is the last element in 'head'. */ \
+ STAILQ_INIT((rest)); \
+ else { \
+ STAILQ_FIRST((rest)) = STAILQ_NEXT((elm), field); \
+ (rest)->stqh_last = (head)->stqh_last; \
+ STAILQ_NEXT((elm), field) = NULL; \
+ (head)->stqh_last = &STAILQ_NEXT((elm), field); \
+ } \
+} while (0)
+
+#define STAILQ_SWAP(head1, head2, type) do { \
+ QUEUE_TYPEOF(type) *_Swap_first = STAILQ_FIRST(head1); \
+ QUEUE_TYPEOF(type) **_Swap_last = (head1)->stqh_last; \
+ STAILQ_FIRST(head1) = STAILQ_FIRST(head2); \
+ (head1)->stqh_last = (head2)->stqh_last; \
+ STAILQ_FIRST(head2) = _Swap_first; \
+ (head2)->stqh_last = _Swap_last; \
+ if (STAILQ_FIRST(head1) == NULL) \
+ (head1)->stqh_last = &STAILQ_FIRST(head1); \
+ if (STAILQ_FIRST(head2) == NULL) \
+ (head2)->stqh_last = &STAILQ_FIRST(head2); \
+} while (0)
+
+#define STAILQ_REVERSE(head, type, field) do { \
+ if (STAILQ_EMPTY(head)) \
+ break; \
+ QUEUE_TYPEOF(type) *_Var, *_Varp, *_Varn; \
+ for (_Var = STAILQ_FIRST(head), _Varp = NULL; \
+ _Var != NULL;) { \
+ _Varn = STAILQ_NEXT(_Var, field); \
+ STAILQ_NEXT(_Var, field) = _Varp; \
+ _Varp = _Var; \
+ _Var = _Varn; \
+ } \
+ (head)->stqh_last = &STAILQ_NEXT(STAILQ_FIRST(head), field); \
+ (head)->stqh_first = _Varp; \
+} while (0)
+
+#define STAILQ_END(head) NULL
+
+
+/*
+ * List declarations.
+ */
+
+#define LIST_HEAD(name, type) \
+struct name { \
+ struct type *lh_first; /* first element */ \
+}
+
+#define LIST_CLASS_HEAD(name, type) \
+struct name { \
+ class type *lh_first; /* first element */ \
+}
+
+#define LIST_HEAD_INITIALIZER(head) \
+ { NULL }
+
+#define LIST_ENTRY(type) \
+struct { \
+ struct type *le_next; /* next element */ \
+ struct type **le_prev; /* address of previous next element */ \
+}
+
+#define LIST_CLASS_ENTRY(type) \
+struct { \
+ class type *le_next; /* next element */ \
+ class type **le_prev; /* address of previous next element */ \
+}
+
+/*
+ * List functions.
+ */
+
+/*
+ * QMD_LIST_CHECK_HEAD(LIST_HEAD *head, LIST_ENTRY NAME)
+ *
+ * If the list is non-empty, validates that the first element of the list
+ * points back at 'head.'
+ */
+#define QMD_LIST_CHECK_HEAD(head, field) \
+ QMD_ASSERT(LIST_FIRST((head)) == NULL || \
+ LIST_FIRST((head))->field.le_prev == \
+ &LIST_FIRST((head)), \
+ "Bad list head %p first->prev != head", \
+ (head))
+
+/*
+ * QMD_LIST_CHECK_NEXT(TYPE *elm, LIST_ENTRY NAME)
+ *
+ * If an element follows 'elm' in the list, validates that the next element
+ * points back at 'elm.'
+ */
+#define QMD_LIST_CHECK_NEXT(elm, field) \
+ QMD_ASSERT(LIST_NEXT((elm), field) == NULL || \
+ LIST_NEXT((elm), field)->field.le_prev == \
+ &((elm)->field.le_next), \
+ "Bad link elm %p next->prev != elm", (elm))
+
+/*
+ * QMD_LIST_CHECK_PREV(TYPE *elm, LIST_ENTRY NAME)
+ *
+ * Validates that the previous element (or head of the list) points to 'elm.'
+ */
+#define QMD_LIST_CHECK_PREV(elm, field) \
+ QMD_ASSERT(*(elm)->field.le_prev == (elm), \
+ "Bad link elm %p prev->next != elm", (elm))
+
+#define LIST_ASSERT_EMPTY(head) \
+ QMD_ASSERT(LIST_EMPTY((head)), \
+ "list %p is not empty", (head))
+
+#define LIST_ASSERT_NONEMPTY(head) \
+ QMD_ASSERT(!LIST_EMPTY((head)), \
+ "list %p is empty", (head))
+
+#define LIST_CONCAT(head1, head2, type, field) do { \
+ QUEUE_TYPEOF(type) *_Curelm = LIST_FIRST(head1); \
+ if (_Curelm == NULL) { \
+ if ((LIST_FIRST(head1) = LIST_FIRST(head2)) != NULL) { \
+ LIST_FIRST(head2)->field.le_prev = \
+ &LIST_FIRST((head1)); \
+ LIST_INIT(head2); \
+ } \
+ } else if (LIST_FIRST(head2) != NULL) { \
+ while (LIST_NEXT(_Curelm, field) != NULL) \
+ _Curelm = LIST_NEXT(_Curelm, field); \
+ LIST_NEXT(_Curelm, field) = LIST_FIRST(head2); \
+ LIST_FIRST(head2)->field.le_prev = &LIST_NEXT(_Curelm, field);\
+ LIST_INIT(head2); \
+ } \
+} while (0)
+
+#define LIST_EMPTY(head) ((head)->lh_first == NULL)
+
+#define LIST_EMPTY_ATOMIC(head) \
+ (atomic_load_ptr(&(head)->lh_first) == NULL)
+
+#define LIST_FIRST(head) ((head)->lh_first)
+
+#define LIST_FOREACH(var, head, field) \
+ for ((var) = LIST_FIRST((head)); \
+ (var); \
+ (var) = LIST_NEXT((var), field))
+
+#define LIST_FOREACH_FROM(var, head, field) \
+ for ((var) = ((var) ? (var) : LIST_FIRST((head))); \
+ (var); \
+ (var) = LIST_NEXT((var), field))
+
+#define LIST_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = LIST_FIRST((head)); \
+ (var) && ((tvar) = LIST_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define LIST_FOREACH_FROM_SAFE(var, head, field, tvar) \
+ for ((var) = ((var) ? (var) : LIST_FIRST((head))); \
+ (var) && ((tvar) = LIST_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define LIST_INIT(head) do { \
+ LIST_FIRST((head)) = NULL; \
+} while (0)
+
+#define LIST_INSERT_AFTER(listelm, elm, field) do { \
+ QMD_LIST_CHECK_NEXT(listelm, field); \
+ if ((LIST_NEXT((elm), field) = LIST_NEXT((listelm), field)) != NULL)\
+ LIST_NEXT((listelm), field)->field.le_prev = \
+ &LIST_NEXT((elm), field); \
+ LIST_NEXT((listelm), field) = (elm); \
+ (elm)->field.le_prev = &LIST_NEXT((listelm), field); \
+} while (0)
+
+#define LIST_INSERT_BEFORE(listelm, elm, field) do { \
+ QMD_LIST_CHECK_PREV(listelm, field); \
+ (elm)->field.le_prev = (listelm)->field.le_prev; \
+ LIST_NEXT((elm), field) = (listelm); \
+ *(listelm)->field.le_prev = (elm); \
+ (listelm)->field.le_prev = &LIST_NEXT((elm), field); \
+} while (0)
+
+#define LIST_INSERT_HEAD(head, elm, field) do { \
+ QMD_LIST_CHECK_HEAD((head), field); \
+ if ((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \
+ LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field);\
+ LIST_FIRST((head)) = (elm); \
+ (elm)->field.le_prev = &LIST_FIRST((head)); \
+} while (0)
+
+#define LIST_NEXT(elm, field) ((elm)->field.le_next)
+
+#define LIST_PREV(elm, head, type, field) \
+ ((elm)->field.le_prev == &LIST_FIRST((head)) ? NULL : \
+ __containerof((elm)->field.le_prev, \
+ QUEUE_TYPEOF(type), field.le_next))
+
+#define LIST_REMOVE_HEAD(head, field) \
+ LIST_REMOVE(LIST_FIRST(head), field)
+
+#define LIST_REMOVE(elm, field) do { \
+ QMD_SAVELINK(_Oldnext, (elm)->field.le_next); \
+ QMD_SAVELINK(_Oldprev, (elm)->field.le_prev); \
+ QMD_LIST_CHECK_NEXT(elm, field); \
+ QMD_LIST_CHECK_PREV(elm, field); \
+ if (LIST_NEXT((elm), field) != NULL) \
+ LIST_NEXT((elm), field)->field.le_prev = \
+ (elm)->field.le_prev; \
+ *(elm)->field.le_prev = LIST_NEXT((elm), field); \
+ TRASHIT(*_Oldnext); \
+ TRASHIT(*_Oldprev); \
+} while (0)
+
+#define LIST_REPLACE(elm, elm2, field) do { \
+ QMD_SAVELINK(_Oldnext, (elm)->field.le_next); \
+ QMD_SAVELINK(_Oldprev, (elm)->field.le_prev); \
+ QMD_LIST_CHECK_NEXT(elm, field); \
+ QMD_LIST_CHECK_PREV(elm, field); \
+ LIST_NEXT((elm2), field) = LIST_NEXT((elm), field); \
+ if (LIST_NEXT((elm2), field) != NULL) \
+ LIST_NEXT((elm2), field)->field.le_prev = \
+ &(elm2)->field.le_next; \
+ (elm2)->field.le_prev = (elm)->field.le_prev; \
+ *(elm2)->field.le_prev = (elm2); \
+ TRASHIT(*_Oldnext); \
+ TRASHIT(*_Oldprev); \
+} while (0)
+
+#define LIST_SPLIT_AFTER(head, elm, rest, field) do { \
+ LIST_ASSERT_NONEMPTY((head)); \
+ if (LIST_NEXT((elm), field) == NULL) \
+ /* 'elm' is the last element in 'head'. */ \
+ LIST_INIT((rest)); \
+ else { \
+ LIST_FIRST((rest)) = LIST_NEXT((elm), field); \
+ LIST_NEXT((elm), field)->field.le_prev = \
+ &LIST_FIRST((rest)); \
+ LIST_NEXT((elm), field) = NULL; \
+ } \
+} while (0)
+
+#define LIST_SWAP(head1, head2, type, field) do { \
+ QUEUE_TYPEOF(type) *swap_tmp = LIST_FIRST(head1); \
+ LIST_FIRST((head1)) = LIST_FIRST((head2)); \
+ LIST_FIRST((head2)) = swap_tmp; \
+ if ((swap_tmp = LIST_FIRST((head1))) != NULL) \
+ swap_tmp->field.le_prev = &LIST_FIRST((head1)); \
+ if ((swap_tmp = LIST_FIRST((head2))) != NULL) \
+ swap_tmp->field.le_prev = &LIST_FIRST((head2)); \
+} while (0)
+
+#define LIST_END(head) NULL
+
+/*
+ * Tail queue declarations.
+ */
+
+#define TAILQ_HEAD(name, type) \
+struct name { \
+ struct type *tqh_first; /* first element */ \
+ struct type **tqh_last; /* addr of last next element */ \
+ TRACEBUF \
+}
+
+#define TAILQ_CLASS_HEAD(name, type) \
+struct name { \
+ class type *tqh_first; /* first element */ \
+ class type **tqh_last; /* addr of last next element */ \
+ TRACEBUF \
+}
+
+#define TAILQ_HEAD_INITIALIZER(head) \
+ { NULL, &(head).tqh_first, TRACEBUF_INITIALIZER }
+
+#define TAILQ_ENTRY(type) \
+struct { \
+ struct type *tqe_next; /* next element */ \
+ struct type **tqe_prev; /* address of previous next element */ \
+ TRACEBUF \
+}
+
+#define TAILQ_CLASS_ENTRY(type) \
+struct { \
+ class type *tqe_next; /* next element */ \
+ class type **tqe_prev; /* address of previous next element */ \
+ TRACEBUF \
+}
+
+/*
+ * Tail queue functions.
+ */
+
+/*
+ * QMD_TAILQ_CHECK_HEAD(TAILQ_HEAD *head, TAILQ_ENTRY NAME)
+ *
+ * If the tailq is non-empty, validates that the first element of the tailq
+ * points back at 'head.'
+ */
+#define QMD_TAILQ_CHECK_HEAD(head, field) \
+ QMD_ASSERT(TAILQ_EMPTY(head) || \
+ TAILQ_FIRST((head))->field.tqe_prev == \
+ &TAILQ_FIRST((head)), \
+ "Bad tailq head %p first->prev != head", \
+ (head))
+
+/*
+ * QMD_TAILQ_CHECK_TAIL(TAILQ_HEAD *head, TAILQ_ENTRY NAME)
+ *
+ * Validates that the tail of the tailq is a pointer to pointer to NULL.
+ */
+#define QMD_TAILQ_CHECK_TAIL(head, field) \
+ QMD_ASSERT(*(head)->tqh_last == NULL, \
+ "Bad tailq NEXT(%p->tqh_last) != NULL", \
+ (head))
+
+/*
+ * QMD_TAILQ_CHECK_NEXT(TYPE *elm, TAILQ_ENTRY NAME)
+ *
+ * If an element follows 'elm' in the tailq, validates that the next element
+ * points back at 'elm.'
+ */
+#define QMD_TAILQ_CHECK_NEXT(elm, field) \
+ QMD_ASSERT(TAILQ_NEXT((elm), field) == NULL || \
+ TAILQ_NEXT((elm), field)->field.tqe_prev == \
+ &((elm)->field.tqe_next), \
+ "Bad link elm %p next->prev != elm", (elm))
+
+/*
+ * QMD_TAILQ_CHECK_PREV(TYPE *elm, TAILQ_ENTRY NAME)
+ *
+ * Validates that the previous element (or head of the tailq) points to 'elm.'
+ */
+#define QMD_TAILQ_CHECK_PREV(elm, field) \
+ QMD_ASSERT(*(elm)->field.tqe_prev == (elm), \
+ "Bad link elm %p prev->next != elm", (elm))
+
+#define TAILQ_ASSERT_EMPTY(head) \
+ QMD_ASSERT(TAILQ_EMPTY((head)), \
+ "tailq %p is not empty", (head))
+
+#define TAILQ_ASSERT_NONEMPTY(head) \
+ QMD_ASSERT(!TAILQ_EMPTY((head)), \
+ "tailq %p is empty", (head))
+
+#define TAILQ_CONCAT(head1, head2, field) do { \
+ if (!TAILQ_EMPTY(head2)) { \
+ *(head1)->tqh_last = (head2)->tqh_first; \
+ (head2)->tqh_first->field.tqe_prev = (head1)->tqh_last; \
+ (head1)->tqh_last = (head2)->tqh_last; \
+ TAILQ_INIT((head2)); \
+ QMD_TRACE_HEAD(head1); \
+ QMD_TRACE_HEAD(head2); \
+ } \
+} while (0)
+
+#define TAILQ_EMPTY(head) ((head)->tqh_first == NULL)
+
+#define TAILQ_EMPTY_ATOMIC(head) \
+ (atomic_load_ptr(&(head)->tqh_first) == NULL)
+
+#define TAILQ_FIRST(head) ((head)->tqh_first)
+
+#define TAILQ_FOREACH(var, head, field) \
+ for ((var) = TAILQ_FIRST((head)); \
+ (var); \
+ (var) = TAILQ_NEXT((var), field))
+
+#define TAILQ_FOREACH_FROM(var, head, field) \
+ for ((var) = ((var) ? (var) : TAILQ_FIRST((head))); \
+ (var); \
+ (var) = TAILQ_NEXT((var), field))
+
+#define TAILQ_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = TAILQ_FIRST((head)); \
+ (var) && ((tvar) = TAILQ_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define TAILQ_FOREACH_FROM_SAFE(var, head, field, tvar) \
+ for ((var) = ((var) ? (var) : TAILQ_FIRST((head))); \
+ (var) && ((tvar) = TAILQ_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \
+ for ((var) = TAILQ_LAST((head), headname); \
+ (var); \
+ (var) = TAILQ_PREV((var), headname, field))
+
+#define TAILQ_FOREACH_REVERSE_FROM(var, head, headname, field) \
+ for ((var) = ((var) ? (var) : TAILQ_LAST((head), headname)); \
+ (var); \
+ (var) = TAILQ_PREV((var), headname, field))
+
+#define TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \
+ for ((var) = TAILQ_LAST((head), headname); \
+ (var) && ((tvar) = TAILQ_PREV((var), headname, field), 1); \
+ (var) = (tvar))
+
+#define TAILQ_FOREACH_REVERSE_FROM_SAFE(var, head, headname, field, tvar)\
+ for ((var) = ((var) ? (var) : TAILQ_LAST((head), headname)); \
+ (var) && ((tvar) = TAILQ_PREV((var), headname, field), 1); \
+ (var) = (tvar))
+
+#define TAILQ_INIT(head) do { \
+ TAILQ_FIRST((head)) = NULL; \
+ (head)->tqh_last = &TAILQ_FIRST((head)); \
+ QMD_TRACE_HEAD(head); \
+} while (0)
+
+#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \
+ QMD_TAILQ_CHECK_NEXT(listelm, field); \
+ if ((TAILQ_NEXT((elm), field) = TAILQ_NEXT((listelm), field)) != NULL)\
+ TAILQ_NEXT((elm), field)->field.tqe_prev = \
+ &TAILQ_NEXT((elm), field); \
+ else { \
+ (head)->tqh_last = &TAILQ_NEXT((elm), field); \
+ QMD_TRACE_HEAD(head); \
+ } \
+ TAILQ_NEXT((listelm), field) = (elm); \
+ (elm)->field.tqe_prev = &TAILQ_NEXT((listelm), field); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+ QMD_TRACE_ELEM(&(listelm)->field); \
+} while (0)
+
+#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \
+ QMD_TAILQ_CHECK_PREV(listelm, field); \
+ (elm)->field.tqe_prev = (listelm)->field.tqe_prev; \
+ TAILQ_NEXT((elm), field) = (listelm); \
+ *(listelm)->field.tqe_prev = (elm); \
+ (listelm)->field.tqe_prev = &TAILQ_NEXT((elm), field); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+ QMD_TRACE_ELEM(&(listelm)->field); \
+} while (0)
+
+#define TAILQ_INSERT_HEAD(head, elm, field) do { \
+ QMD_TAILQ_CHECK_HEAD(head, field); \
+ if ((TAILQ_NEXT((elm), field) = TAILQ_FIRST((head))) != NULL) \
+ TAILQ_FIRST((head))->field.tqe_prev = \
+ &TAILQ_NEXT((elm), field); \
+ else \
+ (head)->tqh_last = &TAILQ_NEXT((elm), field); \
+ TAILQ_FIRST((head)) = (elm); \
+ (elm)->field.tqe_prev = &TAILQ_FIRST((head)); \
+ QMD_TRACE_HEAD(head); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+} while (0)
+
+#define TAILQ_INSERT_TAIL(head, elm, field) do { \
+ QMD_TAILQ_CHECK_TAIL(head, field); \
+ TAILQ_NEXT((elm), field) = NULL; \
+ (elm)->field.tqe_prev = (head)->tqh_last; \
+ *(head)->tqh_last = (elm); \
+ (head)->tqh_last = &TAILQ_NEXT((elm), field); \
+ QMD_TRACE_HEAD(head); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+} while (0)
+
+#define TAILQ_LAST(head, headname) \
+ (*(((struct headname *)((head)->tqh_last))->tqh_last))
+
+/*
+ * The FAST function is fast in that it causes no data access other
+ * then the access to the head. The standard LAST function above
+ * will cause a data access of both the element you want and
+ * the previous element. FAST is very useful for instances when
+ * you may want to prefetch the last data element.
+ */
+#define TAILQ_LAST_FAST(head, type, field) \
+ (TAILQ_EMPTY(head) ? NULL : __containerof((head)->tqh_last, QUEUE_TYPEOF(type), field.tqe_next))
+
+#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next)
+
+#define TAILQ_PREV(elm, headname, field) \
+ (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))
+
+#define TAILQ_PREV_FAST(elm, head, type, field) \
+ ((elm)->field.tqe_prev == &(head)->tqh_first ? NULL : \
+ __containerof((elm)->field.tqe_prev, QUEUE_TYPEOF(type), field.tqe_next))
+
+#define TAILQ_REMOVE_HEAD(head, field) \
+ TAILQ_REMOVE(head, TAILQ_FIRST(head), field)
+
+#define TAILQ_REMOVE(head, elm, field) do { \
+ QMD_SAVELINK(_Oldnext, (elm)->field.tqe_next); \
+ QMD_SAVELINK(_Oldprev, (elm)->field.tqe_prev); \
+ QMD_TAILQ_CHECK_NEXT(elm, field); \
+ QMD_TAILQ_CHECK_PREV(elm, field); \
+ if ((TAILQ_NEXT((elm), field)) != NULL) \
+ TAILQ_NEXT((elm), field)->field.tqe_prev = \
+ (elm)->field.tqe_prev; \
+ else { \
+ (head)->tqh_last = (elm)->field.tqe_prev; \
+ QMD_TRACE_HEAD(head); \
+ } \
+ *(elm)->field.tqe_prev = TAILQ_NEXT((elm), field); \
+ TRASHIT(*_Oldnext); \
+ TRASHIT(*_Oldprev); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+} while (0)
+
+#define TAILQ_REPLACE(head, elm, elm2, field) do { \
+ QMD_SAVELINK(_Oldnext, (elm)->field.tqe_next); \
+ QMD_SAVELINK(_Oldprev, (elm)->field.tqe_prev); \
+ QMD_TAILQ_CHECK_NEXT(elm, field); \
+ QMD_TAILQ_CHECK_PREV(elm, field); \
+ TAILQ_NEXT((elm2), field) = TAILQ_NEXT((elm), field); \
+ if (TAILQ_NEXT((elm2), field) != TAILQ_END(head)) \
+ TAILQ_NEXT((elm2), field)->field.tqe_prev = \
+ &(elm2)->field.tqe_next; \
+ else \
+ (head)->tqh_last = &(elm2)->field.tqe_next; \
+ (elm2)->field.tqe_prev = (elm)->field.tqe_prev; \
+ *(elm2)->field.tqe_prev = (elm2); \
+ TRASHIT(*_Oldnext); \
+ TRASHIT(*_Oldprev); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+} while (0)
+
+#define TAILQ_SPLIT_AFTER(head, elm, rest, field) do { \
+ TAILQ_ASSERT_NONEMPTY((head)); \
+ QMD_TAILQ_CHECK_TAIL((head), field); \
+ if (TAILQ_NEXT((elm), field) == NULL) \
+ /* 'elm' is the last element in 'head'. */ \
+ TAILQ_INIT((rest)); \
+ else { \
+ TAILQ_FIRST((rest)) = TAILQ_NEXT((elm), field); \
+ (rest)->tqh_last = (head)->tqh_last; \
+ TAILQ_NEXT((elm), field)->field.tqe_prev = \
+ &TAILQ_FIRST((rest)); \
+ \
+ TAILQ_NEXT((elm), field) = NULL; \
+ (head)->tqh_last = &TAILQ_NEXT((elm), field); \
+ } \
+} while (0)
+
+#define TAILQ_SWAP(head1, head2, type, field) do { \
+ QUEUE_TYPEOF(type) *swap_first = (head1)->tqh_first; \
+ QUEUE_TYPEOF(type) **swap_last = (head1)->tqh_last; \
+ (head1)->tqh_first = (head2)->tqh_first; \
+ (head1)->tqh_last = (head2)->tqh_last; \
+ (head2)->tqh_first = swap_first; \
+ (head2)->tqh_last = swap_last; \
+ if ((swap_first = (head1)->tqh_first) != NULL) \
+ swap_first->field.tqe_prev = &(head1)->tqh_first; \
+ else \
+ (head1)->tqh_last = &(head1)->tqh_first; \
+ if ((swap_first = (head2)->tqh_first) != NULL) \
+ swap_first->field.tqe_prev = &(head2)->tqh_first; \
+ else \
+ (head2)->tqh_last = &(head2)->tqh_first; \
+} while (0)
+
+#define TAILQ_END(head) NULL
+
+#endif /* !_SYS_QUEUE_H_ */
diff --git a/scripts/windows/mingw-compat/include/sys/random.h b/scripts/windows/mingw-compat/include/sys/random.h
new file mode 100644
index 0000000..c0b22f8
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/sys/random.h
@@ -0,0 +1,9 @@
+#pragma once
+#include
+#ifdef __cplusplus
+extern "C" {
+#endif
+int getrandom(void *buf, size_t buflen, unsigned int flags);
+#ifdef __cplusplus
+}
+#endif
diff --git a/scripts/windows/mingw-compat/include/sys/socket.h b/scripts/windows/mingw-compat/include/sys/socket.h
new file mode 100644
index 0000000..46e36df
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/sys/socket.h
@@ -0,0 +1,8 @@
+#pragma once
+#ifndef _WINSOCK2API_
+#define WIN32_LEAN_AND_MEAN 1
+#include
+#include
+#endif
+#define MSG_NOSIGNAL 0
+#define SHUT_RDWR SD_BOTH
diff --git a/scripts/windows/mingw-compat/include/sys/utsname.h b/scripts/windows/mingw-compat/include/sys/utsname.h
new file mode 100644
index 0000000..2bc6982
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/sys/utsname.h
@@ -0,0 +1,9 @@
+#pragma once
+struct utsname { char sysname[64]; char nodename[64]; char release[64]; char version[128]; char machine[64]; };
+#ifdef __cplusplus
+extern "C" {
+#endif
+int uname(struct utsname *buf);
+#ifdef __cplusplus
+}
+#endif
diff --git a/scripts/windows/mingw-compat/include/syslog.h b/scripts/windows/mingw-compat/include/syslog.h
new file mode 100644
index 0000000..edd4218
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/syslog.h
@@ -0,0 +1,10 @@
+#pragma once
+#define LOG_PID 1
+#define LOG_DAEMON 24
+#define LOG_ERR 3
+#define LOG_WARNING 4
+#define LOG_INFO 6
+#define LOG_DEBUG 7
+static void openlog(const char *ident, int option, int facility) { (void)ident; (void)option; (void)facility; }
+static void syslog(int priority, const char *fmt, ...) { (void)priority; (void)fmt; }
+static int setlogmask(int mask) { (void)mask; return 0; }
diff --git a/scripts/windows/mingw-compat/include/wordexp.h b/scripts/windows/mingw-compat/include/wordexp.h
new file mode 100644
index 0000000..9a7ef16
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/wordexp.h
@@ -0,0 +1,17 @@
+#pragma once
+#include
+#define WRDE_NOCMD 1
+#define WRDE_BADCHAR 1
+#define WRDE_BADVAL 2
+#define WRDE_CMDSUB 3
+#define WRDE_NOSPACE 4
+#define WRDE_SYNTAX 5
+typedef struct { size_t we_wordc; char **we_wordv; size_t we_offs; } wordexp_t;
+#ifdef __cplusplus
+extern "C" {
+#endif
+int wordexp(const char *s, wordexp_t *p, int flags);
+void wordfree(wordexp_t *p);
+#ifdef __cplusplus
+}
+#endif
diff --git a/scripts/windows/mingw-compat/include/zcompat_extra.h b/scripts/windows/mingw-compat/include/zcompat_extra.h
new file mode 100644
index 0000000..8e1899b
--- /dev/null
+++ b/scripts/windows/mingw-compat/include/zcompat_extra.h
@@ -0,0 +1,157 @@
+#pragma once
+/* Force-include for building zapret2 nfq2 (winws2) with mingw-w64.
+ Must be included before ANY system header. */
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN 1
+#endif
+#ifndef _WINSOCK2API_
+#include
+#include
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifndef MAX
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#endif
+#ifndef MIN
+#define MIN(a,b) ((a)<(b)?(a):(b))
+#endif
+#ifndef st_mtim
+#define st_mtim st_mtime
+#endif
+#ifndef GRND_NONBLOCK
+#define GRND_NONBLOCK 1
+#endif
+#ifndef O_NONBLOCK
+#define O_NONBLOCK 0x4000
+#endif
+#ifndef SIGHUP
+#define SIGHUP 1
+#endif
+#ifndef SIGUSR1
+#define SIGUSR1 10
+#endif
+#ifndef SIGUSR2
+#define SIGUSR2 12
+#endif
+#ifndef SIGPIPE
+#define SIGPIPE 13
+#endif
+#ifndef IPVERSION
+#define IPVERSION 4
+#endif
+#ifndef S_IFLNK
+#define S_IFLNK 0xA000
+#endif
+#ifndef S_IFSOCK
+#define S_IFSOCK 0xC000
+#endif
+#ifndef IPTOS_ECN_MASK
+#define IPTOS_ECN_MASK 0x03
+#endif
+#ifndef IPTOS_ECN_ECT1
+#define IPTOS_ECN_ECT1 0x01
+#endif
+#ifndef IPTOS_ECN_ECT0
+#define IPTOS_ECN_ECT0 0x02
+#endif
+#ifndef IPTOS_ECN_CE
+#define IPTOS_ECN_CE 0x03
+#endif
+#ifndef ESHUTDOWN
+#define ESHUTDOWN 108
+#endif
+#ifndef LOG_USER
+#define LOG_USER 8
+#endif
+
+typedef int zc_id_t;
+#define uid_t zc_id_t
+#define gid_t zc_id_t
+typedef unsigned int sigset_t;
+
+#define srandom(s) srand((unsigned)(s))
+
+static const GUID ZC_FOLDERID_LocalAppDataLow = {0xA520A1A4, 0x1780, 0x4FF6, {0xBD, 0x18, 0x16, 0x73, 0x43, 0xC5, 0xAF, 0x16}};
+#ifndef FOLDERID_LocalAppDataLow
+#define FOLDERID_LocalAppDataLow ZC_FOLDERID_LocalAppDataLow
+#endif
+
+static long zc_random(void) { return (long)rand(); }
+#define random zc_random
+
+static void *zc_memmem(const void *h, size_t hl, const void *n, size_t nl) {
+ const char *p;
+ if (!nl) return (void *)h;
+ if (nl > hl) return 0;
+ for (p = h; hl >= nl; p++, hl--) if (!memcmp(p, n, nl)) return (void *)p;
+ return 0;
+}
+#define memmem zc_memmem
+
+static char *zc_strcasestr(const char *h, const char *n) {
+ size_t nl = strlen(n);
+ for (; *h; h++) {
+ size_t i;
+ for (i = 0; i < nl; i++) {
+ char a = h[i], b = n[i];
+ if (a >= 'A' && a <= 'Z') a += 32;
+ if (b >= 'A' && b <= 'Z') b += 32;
+ if (a != b) break;
+ }
+ if (i == nl) return (char *)h;
+ }
+ return 0;
+}
+#define strcasestr zc_strcasestr
+
+static struct tm *zc_localtime_r(const time_t *t, struct tm *r) { return localtime_s(r, t) ? NULL : r; }
+#define localtime_r zc_localtime_r
+static struct tm *zc_gmtime_r(const time_t *t, struct tm *r) { return gmtime_s(r, t) ? NULL : r; }
+#define gmtime_r zc_gmtime_r
+static time_t zc_timegm(struct tm *t) { return _mkgmtime(t); }
+#define timegm zc_timegm
+
+static int zc_setenv(const char *n, const char *v, int ow) { (void)ow; return SetEnvironmentVariableA(n, v) ? 0 : -1; }
+#define setenv zc_setenv
+
+static char *zc_realpath(const char *p, char *out) {
+ char tmp[4096];
+ if (_fullpath(tmp, p, sizeof(tmp) - 1)) {
+ if (!out) out = malloc(4096);
+ if (out) strcpy(out, tmp);
+ return out;
+ }
+ return NULL;
+}
+#define realpath zc_realpath
+
+/* sigaction emulation over signal(); struct tag and function name live in
+ separate C namespaces, so no macros are needed. */
+struct sigaction {
+ void (*sa_handler)(int);
+ unsigned int sa_mask;
+ int sa_flags;
+ void (*sa_sigaction)(int, void *, void *);
+};
+#define sigemptyset(m) (*(m) = 0)
+static int sigaction(int sig, const struct sigaction *act, struct sigaction *old) {
+ (void)old;
+ return signal(sig, act ? act->sa_handler : SIG_DFL) == SIG_ERR ? -1 : 0;
+}
+
+static int zc_getsockopt_wrap(SOCKET s, int l, int o, void *v, int *n) { return getsockopt(s, l, o, (char *)v, n); }
+static int zc_setsockopt_wrap(SOCKET s, int l, int o, const void *v, int n) { return setsockopt(s, l, o, (const char *)v, n); }
+#define getsockopt zc_getsockopt_wrap
+#define setsockopt zc_setsockopt_wrap
+
+#define mkdir(p, m) _mkdir(p)
diff --git a/scripts/windows/patches/zapret2-mingw.patch b/scripts/windows/patches/zapret2-mingw.patch
new file mode 100644
index 0000000..13531bd
--- /dev/null
+++ b/scripts/windows/patches/zapret2-mingw.patch
@@ -0,0 +1,207 @@
+diff --git a/nfq2/helpers.c b/nfq2/helpers.c
+index 3c27805..684a5c6 100644
+--- a/nfq2/helpers.c
++++ b/nfq2/helpers.c
+@@ -13,7 +13,7 @@
+ #include
+ #include
+
+-#ifdef __CYGWIN__
++#ifdef ZAPRET_WIN
+ #include
+ #endif
+
+@@ -435,7 +435,12 @@ bool file_mod_signature(const char *filename, file_mod_sig *ms)
+ FILE_MOD_RESET(ms);
+ return false;
+ }
++#ifdef ZAPRET_WIN
++ ms->mod_time.tv_sec = st.st_mtime;
++ ms->mod_time.tv_nsec = 0;
++#else
+ ms->mod_time = st.st_mtim;
++#endif
+ ms->size = st.st_size;
+ return true;
+ }
+@@ -563,7 +568,7 @@ bool fill_crypto_random_bytes(uint8_t *p, size_t sz)
+ ssize_t rd;
+ int fd;
+
+-#if defined(__linux__) || defined(__CYGWIN__)
++#if defined(__linux__) || defined(ZAPRET_WIN)
+ for (; sz && (rd = getrandom(p, sz, GRND_NONBLOCK)) > 0; p += rd, sz -= rd);
+ if (sz)
+ #elif defined(BSD)
+@@ -678,7 +683,7 @@ bool realpath_any(const char *file, char *pabs)
+ int res;
+ size_t l;
+
+-#ifdef __CYGWIN__
++#ifdef ZAPRET_WIN
+ l = cygwin_conv_path(CCP_WIN_A_TO_POSIX | CCP_ABSOLUTE, file, NULL, 0);
+ char *rp_file = (char*)malloc(l);
+ if (cygwin_conv_path(CCP_WIN_A_TO_POSIX | CCP_ABSOLUTE, file, rp_file, l))
+@@ -704,7 +709,7 @@ bool realpath_any(const char *file, char *pabs)
+ b = res>0 && res
+
+-#elif defined(__CYGWIN__)
++#elif defined(ZAPRET_WIN)
+
++#include
++#include
+ #include
+-
+-// header hell conflicts between unix and win32
+-typedef struct in6_addr IN6_ADDR, *PIN6_ADDR, *LPIN6_ADDR;
+-typedef struct sockaddr *LPSOCKADDR;
+-typedef struct _SOCKET_ADDRESS {
+- LPSOCKADDR lpSockaddr;
+- int iSockaddrLength;
+-} SOCKET_ADDRESS,*PSOCKET_ADDRESS,*LPSOCKET_ADDRESS;
+-#define _WINSOCK2API_
+-#define _NETIOAPI_H_
+ #include
+
+ #endif
+@@ -794,7 +786,7 @@ static int luacall_gettid(lua_State *L)
+ lua_pushinteger(L, tid);
+ #elif defined(__linux__)
+ lua_pushinteger(L, syscall(SYS_gettid));
+-#elif defined(__CYGWIN__)
++#elif defined(ZAPRET_WIN)
+ lua_pushinteger(L, GetCurrentThreadId());
+ #else
+ // unsupported OS ?
+@@ -3131,7 +3123,7 @@ static int luacall_get_source_ip(lua_State *L)
+ LUA_STACK_GUARD_RETURN(L,1)
+ }
+
+-#ifdef __CYGWIN__
++#ifdef ZAPRET_WIN
+ #define GAA_FLAGS (GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_ANYCAST)
+ static int lua_get_ifaddrs(lua_State *L)
+ {
+@@ -3223,7 +3215,7 @@ static int lua_get_ifaddrs(lua_State *L)
+ lua_Integer li;
+ struct ifreq ifr;
+ const char *ifname;
+-#ifdef __CYGWIN__
++#ifdef ZAPRET_WIN
+ char ifname_buf[IFNAMSIZ];
+ #endif
+ memset(&ifr,0,sizeof(ifr));
+@@ -3239,7 +3231,7 @@ static int lua_get_ifaddrs(lua_State *L)
+ {
+ if (a->ifa_addr && (a->ifa_addr->sa_family==AF_INET || a->ifa_addr->sa_family==AF_INET6) && a->ifa_name && *a->ifa_name)
+ {
+-#ifdef __CYGWIN__
++#ifdef ZAPRET_WIN
+ // cygwin returns GUID interface names. windivert needs ifindex.subindex
+ if (index = if_nametoindex(a->ifa_name))
+ {
+@@ -3255,7 +3247,7 @@ static int lua_get_ifaddrs(lua_State *L)
+ lua_pop(L,1);
+ lua_pushf_table(L,ifname);
+ lua_getfield(L,-1,ifname);
+-#ifdef __CYGWIN__
++#ifdef ZAPRET_WIN
+ lua_pushf_str(L, "guid", a->ifa_name);
+ #else
+ index = if_nametoindex(ifname);
+@@ -3750,7 +3742,11 @@ static int luacall_stat(lua_State *L)
+ lua_pushf_lint(L,"dev", st.st_dev);
+ lua_pushf_lint(L,"inode", st.st_ino);
+ lua_pushf_lint(L,"size", st.st_size);
+- lua_pushf_number(L,"mtime", st.st_mtim.tv_sec + st.st_mtim.tv_nsec/1000000000.);
++ #ifdef ZAPRET_WIN
++ lua_pushf_number(L,"mtime", st.st_mtime);
++#else
++ lua_pushf_number(L,"mtime", st.st_mtim.tv_sec + st.st_mtim.tv_nsec/1000000000.);
++#endif
+
+ const char *ftype;
+ switch(st.st_mode & S_IFMT)
+@@ -3791,7 +3787,11 @@ static void lua_xtime(lua_State *L, struct tm *(*timefunc)(const time_t *,struct
+ lua_pushf_int(L,"wday", t.tm_wday);
+ lua_pushf_int(L,"yday", t.tm_yday);
+ lua_pushf_int(L,"isdst", t.tm_isdst);
+- lua_pushf_str(L,"zone", t.tm_zone);
++ #ifdef ZAPRET_WIN
++ lua_pushf_str(L,"zone", _tzname[0]);
++#else
++ lua_pushf_str(L,"zone", t.tm_zone);
++#endif
+
+ char s[40];
+ snprintf(s,sizeof(s),"%02d.%02d.%04d %02d:%02d:%02d", t.tm_mday, t.tm_mon + 1, t.tm_year + 1900, t.tm_hour, t.tm_min, t.tm_sec);
+@@ -4142,11 +4142,11 @@ bool lua_test_init_script_files(void)
+ {
+ if (str->str[0]=='@' && !lua_file_open_test(str->str+1, NULL, NULL))
+ {
+-#ifndef __CYGWIN__
++#ifndef ZAPRET_WIN
+ int e = errno;
+ #endif
+ DLOG_ERR("LUA file '%s' or '%s.gz' not accessible\n", str->str+1, str->str+1);
+-#ifndef __CYGWIN__
++#ifndef ZAPRET_WIN
+ if (e==EACCES)
+ DLOG_ERR("I drop my privileges and do not run Lua as root\ncheck file permissions and +x rights on all directories in the path\n");
+ #endif
+diff --git a/nfq2/sec.c b/nfq2/sec.c
+index 3c675c5..e72a74a 100644
+--- a/nfq2/sec.c
++++ b/nfq2/sec.c
+@@ -236,7 +236,7 @@ bool dropcaps(void)
+
+ #endif // __linux__
+
+-#ifndef __CYGWIN__
++#ifndef ZAPRET_WIN
+
+ #ifndef __linux__
+ bool sec_harden(void)
+@@ -332,6 +332,9 @@ void print_id(void)
+
+ void daemonize(void)
+ {
++#ifdef ZAPRET_WIN
++ return; // backgrounding is handled by the Windows service wrapper
++#else
+ int pid;
+ char cwd[PATH_MAX];
+
+@@ -395,8 +398,8 @@ void daemonize(void)
+ DLOG_PERROR("dup(stderr)");
+ exit(25);
+ }
++#endif
+ }
+-
+ bool writepid(const char *filename)
+ {
+ FILE *F;
diff --git a/third_party/windivert/x64/LICENSE b/third_party/windivert/x64/LICENSE
new file mode 100644
index 0000000..8489a8e
--- /dev/null
+++ b/third_party/windivert/x64/LICENSE
@@ -0,0 +1,1191 @@
+WinDivert is dual-licensed under your choice of the GNU Lesser General Public
+License (LGPL) Version 3 or the GNU General Public License (GPL) Version 2.
+Copies of the LGPLv3, GPLv3 and GPLv2 are provided below.
+
+==============================================================================
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
+
+==============================================================================
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
+
+==============================================================================
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+
diff --git a/third_party/windivert/x64/WinDivert.dll b/third_party/windivert/x64/WinDivert.dll
new file mode 100755
index 0000000..50ca874
Binary files /dev/null and b/third_party/windivert/x64/WinDivert.dll differ
diff --git a/third_party/windivert/x64/WinDivert64.sys b/third_party/windivert/x64/WinDivert64.sys
new file mode 100644
index 0000000..218ccaf
Binary files /dev/null and b/third_party/windivert/x64/WinDivert64.sys differ