diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
new file mode 100755
index 0000000..c7764bf
--- /dev/null
+++ b/.github/workflows/build-release.yml
@@ -0,0 +1,303 @@
+name: Build WDTT Release
+
+on:
+ push:
+ branches:
+ - "**"
+ workflow_dispatch:
+
+jobs:
+ build-release:
+ name: Build release APK
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: read
+
+ env:
+ ANDROID_MIN_API: "29"
+ ANDROID_COMPILE_SDK: "35"
+ ANDROID_BUILD_TOOLS: "35.0.0"
+ ANDROID_NDK_VERSION: "27.2.12479018"
+ SERVER_BINARY_NAME: "server"
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Java 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "17"
+
+ - name: Set up Android SDK
+ uses: android-actions/setup-android@v3
+
+ - name: Install Android SDK packages
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ sdkmanager \
+ "platforms;android-${ANDROID_COMPILE_SDK}" \
+ "build-tools;${ANDROID_BUILD_TOOLS}" \
+ "ndk;${ANDROID_NDK_VERSION}"
+
+ echo "ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}" >> "$GITHUB_ENV"
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.26.x"
+ cache: true
+ cache-dependency-path: |
+ go.mod
+ go_client/go.mod
+
+ - name: Generate Go sums
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ echo "== Root Go module =="
+ go mod tidy
+
+ echo "== Go client module =="
+ cd go_client
+ go mod tidy
+
+ - name: Build Linux server binaries
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ mkdir -p build/server
+
+ echo "== Build server linux/amd64 =="
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
+ go build \
+ -trimpath \
+ -ldflags="-s -w -checklinkname=0" \
+ -o build/server/wdtt-server-linux-amd64 \
+ ./server.go
+
+ echo "== Build server linux/arm64 =="
+ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
+ go build \
+ -trimpath \
+ -ldflags="-s -w -checklinkname=0" \
+ -o build/server/wdtt-server-linux-arm64 \
+ ./server.go
+
+ chmod +x build/server/wdtt-server-linux-amd64
+ chmod +x build/server/wdtt-server-linux-arm64
+
+ - name: Put server binary into Android assets
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ mkdir -p app/src/main/assets
+
+ # Android deploy code expects this exact asset name.
+ cp build/server/wdtt-server-linux-amd64 app/src/main/assets/${SERVER_BINARY_NAME}
+ chmod +x app/src/main/assets/${SERVER_BINARY_NAME}
+
+ echo "Server asset:"
+ ls -lh app/src/main/assets/${SERVER_BINARY_NAME}
+
+ - name: Build Android Go client libraries
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ NDK_ROOT="${ANDROID_NDK_HOME}"
+ TOOLCHAIN="$NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin"
+
+ if [ ! -d "$TOOLCHAIN" ]; then
+ echo "Android NDK toolchain not found: $TOOLCHAIN"
+ exit 1
+ fi
+
+ echo "Using NDK: $NDK_ROOT"
+ echo "Using toolchain: $TOOLCHAIN"
+
+ mkdir -p app/src/main/jniLibs/arm64-v8a
+ mkdir -p app/src/main/jniLibs/armeabi-v7a
+ mkdir -p app/src/main/jniLibs/x86_64
+ mkdir -p build/go-client
+
+ cd go_client
+
+ echo "== Build libclient.so for arm64-v8a =="
+ CGO_ENABLED=1 \
+ GOOS=android \
+ GOARCH=arm64 \
+ CC="$TOOLCHAIN/aarch64-linux-android${ANDROID_MIN_API}-clang" \
+ go build \
+ -buildmode=c-shared \
+ -trimpath \
+ -ldflags="-s -w -checklinkname=0" \
+ -o ../app/src/main/jniLibs/arm64-v8a/libclient.so \
+ .
+
+ cp ../app/src/main/jniLibs/arm64-v8a/libclient.so \
+ ../build/go-client/libclient-arm64-v8a.so
+
+ echo "== Build libclient.so for armeabi-v7a =="
+ CGO_ENABLED=1 \
+ GOOS=android \
+ GOARCH=arm \
+ GOARM=7 \
+ CC="$TOOLCHAIN/armv7a-linux-androideabi${ANDROID_MIN_API}-clang" \
+ go build \
+ -buildmode=c-shared \
+ -trimpath \
+ -ldflags="-s -w -checklinkname=0" \
+ -o ../app/src/main/jniLibs/armeabi-v7a/libclient.so \
+ .
+
+ cp ../app/src/main/jniLibs/armeabi-v7a/libclient.so \
+ ../build/go-client/libclient-armeabi-v7a.so
+
+ echo "== Build libclient.so for x86_64 =="
+ CGO_ENABLED=1 \
+ GOOS=android \
+ GOARCH=amd64 \
+ CC="$TOOLCHAIN/x86_64-linux-android${ANDROID_MIN_API}-clang" \
+ go build \
+ -buildmode=c-shared \
+ -trimpath \
+ -ldflags="-s -w -checklinkname=0" \
+ -o ../app/src/main/jniLibs/x86_64/libclient.so \
+ .
+
+ cp ../app/src/main/jniLibs/x86_64/libclient.so \
+ ../build/go-client/libclient-x86_64.so
+
+ cd ..
+
+ echo "Built JNI libraries:"
+ find app/src/main/jniLibs -type f -name "libclient.so" -exec ls -lh {} \;
+
+ - name: Make Gradle wrapper executable
+ shell: bash
+ run: chmod +x ./gradlew
+
+ - name: Generate temporary release keystore
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ KEYSTORE_PASSWORD="wdtt_temp_release_password"
+ KEY_PASSWORD="$KEYSTORE_PASSWORD"
+ KEY_ALIAS="wdtt-release"
+
+ rm -f release.keystore local.properties signing.env
+
+ keytool -genkeypair -v -keystore release.keystore -storetype PKCS12 -storepass "$KEYSTORE_PASSWORD" -keypass "$KEY_PASSWORD" -alias "$KEY_ALIAS" -keyalg RSA -keysize 4096 -validity 10000 -dname "CN=WDTT,O=WDTT,C=LV"
+
+ {
+ echo "KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD"
+ echo "KEY_PASSWORD=$KEY_PASSWORD"
+ echo "KEY_ALIAS=$KEY_ALIAS"
+ } > signing.env
+
+ echo "Generated temporary release keystore"
+ ls -lh release.keystore
+
+ - name: Build Android release APK
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ # No local.properties is created here, so Gradle produces unsigned release APKs.
+ # The workflow signs them manually in the next step using apksigner.
+ ./gradlew clean :app:assembleRelease --stacktrace
+
+ - name: Sign release APKs manually
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ source signing.env
+
+ BUILD_TOOLS="$(find "$ANDROID_HOME/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1)"
+ ZIPALIGN="$BUILD_TOOLS/zipalign"
+ APKSIGNER="$BUILD_TOOLS/apksigner"
+
+ echo "Using zipalign: $ZIPALIGN"
+ echo "Using apksigner: $APKSIGNER"
+
+ mkdir -p build/signed-apk
+ shopt -s nullglob
+ unsigned_apks=(app/build/outputs/apk/release/*-unsigned.apk)
+
+ if [ ${#unsigned_apks[@]} -eq 0 ]; then
+ echo "No unsigned release APKs found"
+ echo "Available APK outputs:"
+ find app/build/outputs/apk -type f -name "*.apk" -print || true
+ exit 1
+ fi
+
+ for apk in "${unsigned_apks[@]}"; do
+ name="$(basename "$apk" -unsigned.apk)"
+ aligned="build/signed-apk/${name}-aligned.apk"
+ signed="build/signed-apk/${name}-signed.apk"
+
+ echo "== Align $apk =="
+ "$ZIPALIGN" -p -f 4 "$apk" "$aligned"
+
+ echo "== Sign $aligned =="
+ "$APKSIGNER" sign --ks release.keystore --ks-key-alias "$KEY_ALIAS" --ks-pass "pass:$KEYSTORE_PASSWORD" --key-pass "pass:$KEY_PASSWORD" --v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true --v4-signing-enabled false --out "$signed" "$aligned"
+
+ echo "== Verify $signed =="
+ "$APKSIGNER" verify --verbose --print-certs "$signed"
+ done
+
+ echo "Signed APKs:"
+ ls -lh build/signed-apk/*-signed.apk
+
+ - name: Collect build outputs
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ mkdir -p build/artifacts/apk
+ mkdir -p build/artifacts/server
+ mkdir -p build/artifacts/go-client
+
+ echo "== Signed APK outputs =="
+ find build/signed-apk -type f -name "*-signed.apk" -print -exec ls -lh {} \;
+
+ cp build/signed-apk/*-signed.apk build/artifacts/apk/
+ cp build/server/* build/artifacts/server/
+ cp build/go-client/* build/artifacts/go-client/
+
+ echo "== Final artifacts =="
+ find build/artifacts -type f -exec ls -lh {} \;
+
+ - name: Upload release APK artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: WDTT-release-apk
+ path: build/artifacts/apk/*.apk
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Upload server binaries artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: WDTT-server-binaries
+ path: build/artifacts/server/*
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Upload Go client libraries artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: WDTT-go-client-libraries
+ path: build/artifacts/go-client/*
+ if-no-files-found: error
+ retention-days: 14
diff --git a/README.md b/README.md
index 9ca2cc6..f5cc170 100644
--- a/README.md
+++ b/README.md
@@ -13,10 +13,21 @@
**WDTT** — это Android-приложение для создания защищённого **WireGuard-туннеля поверх TURN/DTLS**. Клиент поднимает локальный VPN-интерфейс на устройстве, получает WireGuard-конфигурацию от вашего VPS и передаёт транспорт через TURN-серверы VK, маскируя соединение под обычный зашифрованный медиатрафик звонка.
----
-
+## Содержание
+
+- [Возможности Android-версии](#возможности-android-версии)
+- [Что нового в версии 1.1.8](#что-нового-в-версии-118)
+- [**Другие рабочие решения**](#другие-рабочие-решения)
+- [Как это работает](#как-это-работает)
+- [Быстрый старт](#быстрый-старт)
+- [Получение VK-хеша](#получение-vk-хеша)
+- [Деплой VPS](#деплой-vps)
+- [Управление доступом](#управление-доступом)
+- [Дополнительные возможности](#дополнительные-возможности)
+- [Лицензия](#лицензия)
+
## Возможности Android-версии
- **Полноценный VPN-режим:** приложение использует `VpnService` и WireGuard GoBackend, поэтому трафик выбранных приложений проходит через системный VPN-интерфейс без ручного импорта конфигов.
@@ -53,7 +64,18 @@
* **Автообновление:** исправлена проверка обновлений, которая раньше могла выполняться только один раз при старте приложения.
* **Keepalive:** добавлен DTLS keepalive для длительных сессий, чтобы снизить вероятность `reader EOF`.
----
+## Другие рабочие решения
+
+Рабочие решения с обходом шейпинга скорости и частыми обновлениями:
+
+**Моё любимое**
+- [Moroka8/vk-turn-proxy](https://github.com/Moroka8/vk-turn-proxy) — Качественные ядра клиента и сервера
+
+**Android**
+- [samosvalishe/turn-proxy-android](https://github.com/samosvalishe/turn-proxy-android) — альтернативная андроид версия
+
+**iOS**
+- [anton48/vk-turn-proxy-ios](https://github.com/anton48/vk-turn-proxy-ios) — версия под ios
## Как это работает
@@ -70,6 +92,20 @@ Android-приложение → VpnService / WireGuard GoBackend → локал
6. Android-часть парсит полученный WireGuard-конфиг, поднимает системный VPN-туннель и применяет исключения приложений.
7. Watchdog следит за Go-процессом, активными воркерами и сетевыми изменениями, перезапуская транспорт при сбоях.
+