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 @@
-![FDPI — Fast DPI Bypass](docs/assets/banner.png) - [![Linux](https://img.shields.io/badge/Linux-CachyOS%20%2F%20Arch-64d9f2?style=for-the-badge&labelColor=10111b)](docs/INSTALL_RU.md) [![Go](https://img.shields.io/badge/Go-1.24%2B-ad91ff?style=for-the-badge&labelColor=10111b)](go.mod) [![License](https://img.shields.io/badge/License-MIT-79ee9a?style=for-the-badge&labelColor=10111b)](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