diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 505b1ad..3013fe9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -43,7 +43,7 @@ jobs:
cgo: 0
- goos: linux
goarch: riscv64
- cgo: 0
+ cgo: 0
- goos: darwin
goarch: amd64
cgo: 0
@@ -135,7 +135,7 @@ jobs:
go build -ldflags "-s -w -checklinkname=0" -trimpath \
-o "dist/client-${GOOS}-${GOARCH}${EXT}" \
./client
-
+
GOOS=$GOOS GOARCH=$GOARCH GOARM=$GOARM GOMIPS=$GOMIPS \
go build -ldflags "-s -w -checklinkname=0" -trimpath \
-o "dist/server-${GOOS}-${GOARCH}${EXT}" \
@@ -243,21 +243,21 @@ jobs:
{
if [ -n "$BREAKING" ]; then
- echo "### Несовместимые изменения"
+ echo "### Breaking Changes"
echo
printf '%s\n' "$BREAKING" | sed 's/^/- /'
echo
fi
if [ -n "$FEATURES" ]; then
- echo "### Новые функции"
+ echo "### Features"
echo
printf '%s\n' "$FEATURES" | sed 's/^/- /'
echo
fi
if [ -n "$FIXES" ]; then
- echo "### Исправление багов"
+ echo "### Bug Fixes"
echo
printf '%s\n' "$FIXES" | sed 's/^/- /'
echo
@@ -298,10 +298,10 @@ jobs:
uses: actions/checkout@v6
- name: Set up QEMU
- uses: docker/setup-qemu-action@v3
+ uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
- name: Prepare image name
id: image
@@ -311,7 +311,7 @@ jobs:
echo "name=ghcr.io/$(echo "$REPOSITORY" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
+ uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -319,7 +319,7 @@ jobs:
- name: Extract Docker metadata
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: ${{ steps.image.outputs.name }}
tags: |
@@ -327,7 +327,7 @@ jobs:
type=raw,value=${{ needs.release.outputs.tag_name }}
- name: Build and push Docker image
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
diff --git a/README.md b/README.md
index 983e4e6..6a24c2b 100644
--- a/README.md
+++ b/README.md
@@ -5,21 +5,35 @@
Только для учебных целей!
## Похожие проекты
-- https://github.com/MYSOREZ/vk-turn-proxy-android - клиент для андроида
-- https://github.com/kiper292/wireguard-turn-android - клиент для андроида интегрированный в WireGuard
-- https://github.com/WINGS-N/WINGSV - клиент для андроида с One UI, WireGuard, раздачей VPN с root
-- https://github.com/nullcstring/turnbridge - клиент для IOS
+
+> [!WARNING]
+> Авторы данного репозитория не несут ответственности за другие похожие проекты.
+
+#### Server
- https://github.com/Urtyom-Alyanov/turn-proxy - реализация на Rust
- https://github.com/jaykaiperson/lionheart - аналог для https://stream.wb.ru (статья: https://habr.com/ru/articles/1017410/)
- https://github.com/kulikov0/whitelist-bypass - проброс через медиасервер SFU ВК и Яндекс Телемоста
+#### Android
+- https://github.com/MYSOREZ/vk-turn-proxy-android - клиент для андроида
+- https://github.com/WINGS-N/WINGSV - клиент для андроида с One UI, WireGuard, раздачей VPN с root
+- https://github.com/kiper292/wireguard-turn-android - клиент для андроида интегрированный в WireGuard
+- https://github.com/oxsidee/vkpn - клиент для андроида (кроссплатформенный Flutter)
+
+#### iOS
+- https://github.com/nullcstring/turnbridge - клиент для iOS
+
+#### macOS
+- https://github.com/denny4-user/vk-turn-proxy-macos-gui - клиент для macOS
+
+
## Настройка
Нам понадобится:
1. Ссылка на действующий ВК звонок: создаём свой (нужен аккаунт вк), или гуглим `"https://vk.com/call/join/"`.
Ссылка действительна вечно, если не нажимать "завершить звонок для всех"
-2. Или ссыска на звонок Яндекс телемоста: `"https://telemost.yandex.ru/j/"`. Её лучше не гуглить, так как видно подключение к конференции
+2. Или ссылка на звонок Яндекс телемоста: `"https://telemost.yandex.ru/j/"`. Её лучше не гуглить, так как видно подключение к конференции
3. VPS с установленным WireGuard
4. Для андроида: скачать Termux из F-Droid
@@ -123,11 +137,7 @@ docker run -p 56000:56000/udp -e CONNECT_ADDR=192.168.1.10:51820 vk-turn-proxy
#### Android
-**Рекомендуемый способ:**
-Использовать нативное Android-приложение [vk-turn-proxy-android](https://github.com/MYSOREZ/vk-turn-proxy-android).
-
-- В клиентском конфиге WireGuard меняем адрес сервера на `127.0.0.1:9000`, ставим MTU 1280
-- **Добавляем приложение в исключения WireGuard. Нажимаем "сохранить".**
+См. [клиенты](#android).
**Альтернативный способ (через Termux):**
@@ -145,7 +155,7 @@ termux-wake-lock
termux-wake-unlock
```
-Скачиваем бинарник в локальную папку, даём права на исполнение, в команде указаана самая популярная архитектура `client-android-arm64`:
+Скачиваем бинарник в локальную папку, даём права на исполнение, в команде указана самая популярная архитектура `client-android-arm64`:
```bash
curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/download/client-android-arm64 && chmod +x client
@@ -165,8 +175,53 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down
**Если после включения VPN в терминале вылезают ошибки DNS, попробуйте в Wireguard включить VPN только для нужных приложений.**
-#### IOS
-- https://github.com/cacggghp/vk-turn-proxy/issues/76
+#### iOS
+
+См. [клиенты](#ios).
+
+**Альтернативный способ (через iSH Shell):**
+
+Скачать приложение [iSH Shell](https://apps.apple.com/ru/app/ish-shell/id1436902243):
+
+```bash
+# Установить curl, если его нет
+apk update
+apk add curl
+```
+
+```bash
+# Скачать бинарник и дать права на запуск
+curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/download/client-linux-386 && chmod +x client
+```
+
+```bash
+# Запустить клиент
+./client -listen 127.0.0.1:9000 -peer :56000 -vk-link
+```
+
+####
+В конфиге WireGuard (WG) есть строка
+```
+AllowedIPs = 0.0.0.0/0, ::0
+```
+Что означает разрешить весь интернет.
+
+Для данной утилиты, нужно исключить IP адреса ВК, т.к. для звонков он подключается к диапазону 155.212.192.0/20, то можно исключить этот диапазон.
+
+В конфиге WG, замените строку AllowedIPs:
+```
+AllowedIPs = 0.0.0.0/1, 128.0.0.0/4, 144.0.0.0/5, 152.0.0.0/7, 154.0.0.0/8, 155.0.0.0/9, 155.128.0.0/10, 155.192.0.0/12, 155.208.0.0/14, 155.212.0.0/17, 155.212.128.0/18, 155.212.208.0/20, 155.212.224.0/19, 155.213.0.0/16, 155.214.0.0/15, 155.216.0.0/13, 155.224.0.0/11, 156.0.0.0/6, 160.0.0.0/3, 192.0.0.0/2
+```
+
+Строка со всеми исключёнными адресами ВК:
+```
+AllowedIPs = 0.0.0.0/6, 4.0.0.0/8, 5.0.0.0/11, 5.32.0.0/12, 5.48.0.0/13, 5.56.0.0/14, 5.60.0.0/16, 5.61.0.0/20, 5.61.24.0/21, 5.61.32.0/19, 5.61.64.0/18, 5.61.128.0/18, 5.61.192.0/19, 5.61.224.0/21, 5.61.240.0/20, 5.62.0.0/15, 5.64.0.0/11, 5.96.0.0/14, 5.100.0.0/16, 5.101.0.0/19, 5.101.32.0/21, 5.101.44.0/22, 5.101.48.0/20, 5.101.64.0/18, 5.101.128.0/17, 5.102.0.0/15, 5.104.0.0/13, 5.112.0.0/12, 5.128.0.0/11, 5.160.0.0/12, 5.176.0.0/14, 5.180.0.0/16, 5.181.0.0/19, 5.181.32.0/20, 5.181.48.0/21, 5.181.56.0/22, 5.181.64.0/18, 5.181.128.0/17, 5.182.0.0/15, 5.184.0.0/14, 5.188.0.0/17, 5.188.128.0/21, 5.188.136.0/22, 5.188.144.0/20, 5.188.160.0/19, 5.188.192.0/18, 5.189.0.0/16, 5.190.0.0/15, 5.192.0.0/10, 6.0.0.0/7, 8.0.0.0/5, 16.0.0.0/5, 24.0.0.0/6, 28.0.0.0/7, 30.0.0.0/8, 31.0.0.0/9, 31.128.0.0/11, 31.160.0.0/12, 31.176.0.0/16, 31.177.0.0/18, 31.177.64.0/19, 31.177.96.0/21, 31.177.108.0/22, 31.177.112.0/20, 31.177.128.0/17, 31.178.0.0/15, 31.180.0.0/14, 31.184.0.0/13, 31.192.0.0/10, 32.0.0.0/6, 36.0.0.0/8, 37.0.0.0/9, 37.128.0.0/13, 37.136.0.0/15, 37.138.0.0/16, 37.139.0.0/19, 37.139.36.0/22, 37.139.44.0/22, 37.139.48.0/20, 37.139.64.0/18, 37.139.128.0/17, 37.140.0.0/14, 37.144.0.0/12, 37.160.0.0/11, 37.192.0.0/10, 38.0.0.0/7, 40.0.0.0/6, 44.0.0.0/8, 45.0.0.0/10, 45.64.0.0/12, 45.80.0.0/14, 45.84.0.0/17, 45.84.132.0/22, 45.84.136.0/21, 45.84.144.0/20, 45.84.160.0/19, 45.84.192.0/18, 45.85.0.0/16, 45.86.0.0/15, 45.88.0.0/13, 45.96.0.0/11, 45.128.0.0/13, 45.136.0.0/20, 45.136.16.0/22, 45.136.24.0/21, 45.136.32.0/19, 45.136.64.0/18, 45.136.128.0/17, 45.137.0.0/16, 45.138.0.0/15, 45.140.0.0/14, 45.144.0.0/12, 45.160.0.0/11, 45.192.0.0/10, 46.0.0.0/7, 48.0.0.0/5, 56.0.0.0/6, 60.0.0.0/7, 62.0.0.0/9, 62.128.0.0/10, 62.192.0.0/12, 62.208.0.0/13, 62.216.0.0/16, 62.217.0.0/17, 62.217.128.0/19, 62.217.176.0/20, 62.217.192.0/18, 62.218.0.0/15, 62.220.0.0/14, 62.224.0.0/11, 63.0.0.0/8, 64.0.0.0/5, 72.0.0.0/6, 76.0.0.0/7, 78.0.0.0/8, 79.0.0.0/9, 79.128.0.0/13, 79.136.0.0/16, 79.137.0.0/17, 79.137.128.0/20, 79.137.144.0/21, 79.137.152.0/22, 79.137.156.0/24, 79.137.158.0/23, 79.137.160.0/21, 79.137.168.0/22, 79.137.172.0/23, 79.137.176.0/20, 79.137.192.0/19, 79.137.224.0/20, 79.137.248.0/21, 79.138.0.0/15, 79.140.0.0/14, 79.144.0.0/12, 79.160.0.0/11, 79.192.0.0/10, 80.0.0.0/7, 82.0.0.0/8, 83.0.0.0/9, 83.128.0.0/11, 83.160.0.0/14, 83.164.0.0/15, 83.166.0.0/17, 83.166.128.0/18, 83.166.192.0/19, 83.166.224.0/21, 83.166.240.0/21, 83.167.0.0/16, 83.168.0.0/13, 83.176.0.0/12, 83.192.0.0/12, 83.208.0.0/13, 83.216.0.0/16, 83.217.0.0/17, 83.217.128.0/18, 83.217.192.0/20, 83.217.208.0/21, 83.217.220.0/22, 83.217.224.0/19, 83.218.0.0/15, 83.220.0.0/15, 83.222.0.0/20, 83.222.16.0/21, 83.222.24.0/22, 83.222.32.0/19, 83.222.64.0/18, 83.222.128.0/17, 83.223.0.0/16, 83.224.0.0/11, 84.0.0.0/12, 84.16.0.0/14, 84.20.0.0/15, 84.22.0.0/16, 84.23.0.0/19, 84.23.32.0/20, 84.23.48.0/22, 84.23.56.0/21, 84.23.64.0/18, 84.23.128.0/17, 84.24.0.0/13, 84.32.0.0/11, 84.64.0.0/10, 84.128.0.0/9, 85.0.0.0/9, 85.128.0.0/10, 85.192.0.0/19, 85.192.36.0/22, 85.192.40.0/21, 85.192.48.0/20, 85.192.64.0/18, 85.192.128.0/17, 85.193.0.0/16, 85.194.0.0/15, 85.196.0.0/14, 85.200.0.0/13, 85.208.0.0/12, 85.224.0.0/11, 86.0.0.0/8, 87.0.0.0/9, 87.128.0.0/10, 87.192.0.0/11, 87.224.0.0/13, 87.232.0.0/14, 87.236.0.0/15, 87.238.0.0/16, 87.239.0.0/18, 87.239.64.0/19, 87.239.96.0/21, 87.239.112.0/20, 87.239.128.0/17, 87.240.0.0/15, 87.242.0.0/18, 87.242.64.0/19, 87.242.96.0/20, 87.242.116.0/22, 87.242.120.0/21, 87.242.128.0/17, 87.243.0.0/16, 87.244.0.0/14, 87.248.0.0/13, 88.0.0.0/8, 89.0.0.0/9, 89.128.0.0/10, 89.192.0.0/12, 89.208.0.0/18, 89.208.64.0/20, 89.208.80.0/22, 89.208.88.0/21, 89.208.96.0/19, 89.208.128.0/18, 89.208.192.0/22, 89.208.200.0/21, 89.208.212.0/22, 89.208.224.0/22, 89.208.232.0/21, 89.208.240.0/20, 89.209.0.0/16, 89.210.0.0/15, 89.212.0.0/14, 89.216.0.0/14, 89.220.0.0/16, 89.221.0.0/17, 89.221.128.0/18, 89.221.192.0/19, 89.221.224.0/22, 89.221.240.0/20, 89.222.0.0/15, 89.224.0.0/11, 90.0.0.0/9, 90.128.0.0/12, 90.144.0.0/13, 90.152.0.0/14, 90.156.0.0/17, 90.156.128.0/20, 90.156.144.0/22, 90.156.152.0/21, 90.156.160.0/19, 90.156.192.0/20, 90.156.208.0/22, 90.156.220.0/22, 90.156.224.0/21, 90.156.240.0/20, 90.157.0.0/16, 90.158.0.0/15, 90.160.0.0/11, 90.192.0.0/10, 91.0.0.0/9, 91.128.0.0/10, 91.192.0.0/12, 91.208.0.0/13, 91.216.0.0/15, 91.218.0.0/16, 91.219.0.0/17, 91.219.128.0/18, 91.219.192.0/19, 91.219.228.0/22, 91.219.232.0/21, 91.219.240.0/20, 91.220.0.0/14, 91.224.0.0/14, 91.228.0.0/15, 91.230.0.0/16, 91.231.0.0/17, 91.231.128.0/22, 91.231.136.0/21, 91.231.144.0/20, 91.231.160.0/19, 91.231.192.0/18, 91.232.0.0/13, 91.240.0.0/12, 92.0.0.0/11, 92.32.0.0/14, 92.36.0.0/15, 92.38.0.0/17, 92.38.128.0/18, 92.38.192.0/20, 92.38.208.0/21, 92.38.216.0/24, 92.38.218.0/23, 92.38.220.0/22, 92.38.224.0/19, 92.39.0.0/16, 92.40.0.0/13, 92.48.0.0/12, 92.64.0.0/10, 92.128.0.0/9, 93.0.0.0/8, 94.0.0.0/10, 94.64.0.0/11, 94.96.0.0/14, 94.100.0.0/17, 94.100.128.0/19, 94.100.160.0/20, 94.100.192.0/18, 94.101.0.0/16, 94.102.0.0/15, 94.104.0.0/13, 94.112.0.0/12, 94.128.0.0/13, 94.136.0.0/15, 94.138.0.0/16, 94.139.0.0/17, 94.139.128.0/18, 94.139.192.0/19, 94.139.224.0/20, 94.139.240.0/22, 94.139.248.0/21, 94.140.0.0/14, 94.144.0.0/12, 94.160.0.0/11, 94.192.0.0/10, 95.0.0.0/9, 95.128.0.0/11, 95.160.0.0/15, 95.162.0.0/16, 95.163.0.0/19, 95.163.64.0/18, 95.163.128.0/22, 95.163.132.0/24, 95.163.134.0/23, 95.163.136.0/21, 95.163.144.0/20, 95.163.160.0/20, 95.163.176.0/22, 95.163.184.0/21, 95.163.192.0/20, 95.163.220.0/22, 95.163.224.0/20, 95.163.240.0/21, 95.164.0.0/14, 95.168.0.0/13, 95.176.0.0/12, 95.192.0.0/10, 96.0.0.0/5, 104.0.0.0/6, 108.0.0.0/8, 109.0.0.0/10, 109.64.0.0/11, 109.96.0.0/12, 109.112.0.0/13, 109.120.0.0/17, 109.120.128.0/19, 109.120.160.0/20, 109.120.176.0/22, 109.120.184.0/22, 109.120.192.0/18, 109.121.0.0/16, 109.122.0.0/15, 109.124.0.0/14, 109.128.0.0/9, 110.0.0.0/7, 112.0.0.0/4, 128.0.0.0/9, 128.128.0.0/13, 128.136.0.0/14, 128.140.0.0/17, 128.140.128.0/19, 128.140.160.0/21, 128.140.176.0/20, 128.140.192.0/18, 128.141.0.0/16, 128.142.0.0/15, 128.144.0.0/12, 128.160.0.0/11, 128.192.0.0/10, 129.0.0.0/8, 130.0.0.0/11, 130.32.0.0/12, 130.48.0.0/16, 130.49.0.0/17, 130.49.128.0/18, 130.49.192.0/19, 130.50.0.0/15, 130.52.0.0/14, 130.56.0.0/13, 130.64.0.0/10, 130.128.0.0/9, 131.0.0.0/8, 132.0.0.0/6, 136.0.0.0/5, 144.0.0.0/7, 146.0.0.0/9, 146.128.0.0/11, 146.160.0.0/12, 146.176.0.0/13, 146.184.0.0/16, 146.185.0.0/17, 146.185.128.0/18, 146.185.192.0/20, 146.185.212.0/22, 146.185.216.0/21, 146.185.224.0/20, 146.185.244.0/22, 146.185.248.0/21, 146.186.0.0/15, 146.188.0.0/14, 146.192.0.0/10, 147.0.0.0/8, 148.0.0.0/6, 152.0.0.0/7, 154.0.0.0/8, 155.0.0.0/9, 155.128.0.0/10, 155.192.0.0/12, 155.208.0.0/14, 155.212.0.0/17, 155.212.128.0/18, 155.212.208.0/20, 155.212.224.0/19, 155.213.0.0/16, 155.214.0.0/15, 155.216.0.0/13, 155.224.0.0/11, 156.0.0.0/6, 160.0.0.0/8, 161.0.0.0/10, 161.64.0.0/11, 161.96.0.0/13, 161.104.0.0/18, 161.104.64.0/19, 161.104.96.0/21, 161.104.112.0/20, 161.104.128.0/17, 161.105.0.0/16, 161.106.0.0/15, 161.108.0.0/14, 161.112.0.0/12, 161.128.0.0/9, 162.0.0.0/7, 164.0.0.0/6, 168.0.0.0/5, 176.0.0.0/10, 176.64.0.0/11, 176.96.0.0/12, 176.112.0.0/17, 176.112.128.0/19, 176.112.160.0/21, 176.112.176.0/20, 176.112.192.0/18, 176.113.0.0/16, 176.114.0.0/15, 176.116.0.0/14, 176.120.0.0/13, 176.128.0.0/9, 177.0.0.0/8, 178.0.0.0/12, 178.16.0.0/14, 178.20.0.0/15, 178.22.0.0/18, 178.22.64.0/20, 178.22.80.0/21, 178.22.96.0/19, 178.22.128.0/17, 178.23.0.0/16, 178.24.0.0/13, 178.32.0.0/11, 178.64.0.0/10, 178.128.0.0/10, 178.192.0.0/11, 178.224.0.0/13, 178.232.0.0/14, 178.236.0.0/16, 178.237.0.0/20, 178.237.32.0/19, 178.237.64.0/18, 178.237.128.0/17, 178.238.0.0/15, 178.240.0.0/12, 179.0.0.0/8, 180.0.0.0/6, 184.0.0.0/8, 185.0.0.0/14, 185.4.0.0/16, 185.5.0.0/17, 185.5.128.0/21, 185.5.140.0/22, 185.5.144.0/20, 185.5.160.0/19, 185.5.192.0/18, 185.6.0.0/15, 185.8.0.0/13, 185.16.0.0/17, 185.16.128.0/20, 185.16.144.0/22, 185.16.152.0/21, 185.16.160.0/19, 185.16.192.0/19, 185.16.224.0/20, 185.16.240.0/22, 185.16.248.0/21, 185.17.0.0/16, 185.18.0.0/15, 185.20.0.0/14, 185.24.0.0/13, 185.32.0.0/11, 185.64.0.0/12, 185.80.0.0/14, 185.84.0.0/15, 185.86.0.0/17, 185.86.128.0/20, 185.86.148.0/22, 185.86.152.0/21, 185.86.160.0/19, 185.86.192.0/18, 185.87.0.0/16, 185.88.0.0/13, 185.96.0.0/14, 185.100.0.0/18, 185.100.64.0/19, 185.100.96.0/21, 185.100.108.0/22, 185.100.112.0/20, 185.100.128.0/17, 185.101.0.0/16, 185.102.0.0/15, 185.104.0.0/13, 185.112.0.0/12, 185.128.0.0/15, 185.130.0.0/18, 185.130.64.0/19, 185.130.96.0/20, 185.130.116.0/22, 185.130.120.0/21, 185.130.128.0/17, 185.131.0.0/18, 185.131.64.0/22, 185.131.72.0/21, 185.131.80.0/20, 185.131.96.0/19, 185.131.128.0/17, 185.132.0.0/14, 185.136.0.0/13, 185.144.0.0/12, 185.160.0.0/12, 185.176.0.0/14, 185.180.0.0/17, 185.180.128.0/18, 185.180.192.0/21, 185.180.204.0/22, 185.180.208.0/20, 185.180.224.0/19, 185.181.0.0/16, 185.182.0.0/15, 185.184.0.0/15, 185.186.0.0/16, 185.187.0.0/19, 185.187.32.0/20, 185.187.48.0/21, 185.187.56.0/22, 185.187.60.0/23, 185.187.62.0/24, 185.187.64.0/18, 185.187.128.0/17, 185.188.0.0/14, 185.192.0.0/11, 185.224.0.0/15, 185.226.0.0/19, 185.226.32.0/20, 185.226.48.0/22, 185.226.56.0/21, 185.226.64.0/18, 185.226.128.0/17, 185.227.0.0/16, 185.228.0.0/14, 185.232.0.0/13, 185.240.0.0/16, 185.241.0.0/17, 185.241.128.0/18, 185.241.196.0/22, 185.241.200.0/21, 185.241.208.0/20, 185.241.224.0/19, 185.242.0.0/15, 185.244.0.0/14, 185.248.0.0/13, 186.0.0.0/7, 188.0.0.0/10, 188.64.0.0/12, 188.80.0.0/13, 188.88.0.0/14, 188.92.0.0/16, 188.93.0.0/19, 188.93.32.0/20, 188.93.48.0/21, 188.93.64.0/18, 188.93.128.0/17, 188.94.0.0/15, 188.96.0.0/11, 188.128.0.0/9, 189.0.0.0/8, 190.0.0.0/7, 192.0.0.0/8, 193.0.0.0/9, 193.128.0.0/10, 193.192.0.0/13, 193.200.0.0/15, 193.202.0.0/16, 193.203.0.0/19, 193.203.32.0/21, 193.203.44.0/22, 193.203.48.0/20, 193.203.64.0/18, 193.203.128.0/17, 193.204.0.0/14, 193.208.0.0/12, 193.224.0.0/11, 194.0.0.0/9, 194.128.0.0/11, 194.160.0.0/12, 194.176.0.0/13, 194.184.0.0/15, 194.186.0.0/19, 194.186.32.0/20, 194.186.48.0/21, 194.186.56.0/22, 194.186.60.0/23, 194.186.62.0/24, 194.186.64.0/18, 194.186.128.0/17, 194.187.0.0/16, 194.188.0.0/14, 194.192.0.0/10, 195.0.0.0/9, 195.128.0.0/10, 195.192.0.0/12, 195.208.0.0/15, 195.210.0.0/16, 195.211.0.0/20, 195.211.16.0/22, 195.211.24.0/21, 195.211.32.0/19, 195.211.64.0/18, 195.211.128.0/17, 195.212.0.0/14, 195.216.0.0/15, 195.218.0.0/17, 195.218.128.0/19, 195.218.160.0/20, 195.218.176.0/21, 195.218.184.0/22, 195.218.188.0/23, 195.218.192.0/18, 195.219.0.0/16, 195.220.0.0/14, 195.224.0.0/11, 196.0.0.0/6, 200.0.0.0/5, 208.0.0.0/6, 212.0.0.0/10, 212.64.0.0/11, 212.96.0.0/13, 212.104.0.0/14, 212.108.0.0/15, 212.110.0.0/16, 212.111.0.0/18, 212.111.64.0/20, 212.111.80.0/22, 212.111.88.0/21, 212.111.96.0/19, 212.111.128.0/17, 212.112.0.0/12, 212.128.0.0/10, 212.192.0.0/11, 212.224.0.0/13, 212.232.0.0/16, 212.233.0.0/18, 212.233.64.0/21, 212.233.80.0/21, 212.233.100.0/22, 212.233.104.0/21, 212.233.112.0/21, 212.233.124.0/22, 212.233.128.0/17, 212.234.0.0/15, 212.236.0.0/14, 212.240.0.0/12, 213.0.0.0/9, 213.128.0.0/10, 213.192.0.0/12, 213.208.0.0/13, 213.216.0.0/15, 213.218.0.0/16, 213.219.0.0/17, 213.219.128.0/18, 213.219.192.0/20, 213.219.208.0/22, 213.219.216.0/21, 213.219.224.0/19, 213.220.0.0/14, 213.224.0.0/11, 214.0.0.0/7, 216.0.0.0/8, 217.0.0.0/12, 217.16.0.0/20, 217.16.32.0/19, 217.16.64.0/18, 217.16.128.0/17, 217.17.0.0/16, 217.18.0.0/15, 217.20.0.0/17, 217.20.128.0/20, 217.20.160.0/19, 217.20.192.0/18, 217.21.0.0/16, 217.22.0.0/15, 217.24.0.0/13, 217.32.0.0/11, 217.64.0.0/14, 217.68.0.0/16, 217.69.0.0/17, 217.69.144.0/20, 217.69.160.0/19, 217.69.192.0/18, 217.70.0.0/15, 217.72.0.0/13, 217.80.0.0/12, 217.96.0.0/11, 217.128.0.0/11, 217.160.0.0/13, 217.168.0.0/14, 217.172.0.0/15, 217.174.0.0/17, 217.174.128.0/19, 217.174.160.0/20, 217.174.176.0/21, 217.174.184.0/22, 217.174.192.0/18, 217.175.0.0/16, 217.176.0.0/12, 217.192.0.0/10, 218.0.0.0/7, 220.0.0.0/6, 224.0.0.0/3
+```
+
+Утилита, где самому можно добавить ещё свои адреса в исключения: https://www.procustodibus.com/blog/2021/03/wireguard-allowedips-calculator/
+
+Конфиг WG с изменённым полем `AllowedIPs` добавить в любом клиенте WG и запустить.
+
#### Linux
В клиентском конфиге WireGuard меняем адрес сервера на `127.0.0.1:9000`, ставим MTU 1280
@@ -183,6 +238,36 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down
Не включайте впн, пока программа не установит соединение! В отличие от андроида, здесь часть запросов будет идти через впн (dns и запрос подключения к turn)
+#### macOS
+
+См. [клиенты](#macos).
+
+**Альтернативный способ (через Terminal):**
+
+- В клиентском конфиге WireGuard меняем адрес сервера на 127.0.0.1:9000, ставим MTU 1280.
+- Добавляем Terminal в исключения WireGuard. Нажимаем "сохранить".
+
+В Terminal:
+
+Скачать бинарник (Apple Silicon):
+
+```bash
+curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/download/client-darwin-arm64 && chmod +x client
+```
+
+Скачать бинарник (Intel):
+
+```bash
+curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/download/client-darwin-amd64 && chmod +x client
+```
+
+Запустить клиент
+
+```bash
+./client -listen 127.0.0.1:9000 -peer :56000 -vk-link
+```
+
+
#### Windows
В клиентском конфиге WireGuard меняем адрес сервера на `127.0.0.1:9000`, ставим MTU 1280
@@ -205,7 +290,7 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down
Если не работает TCP, попробуйте добавить флаг `-udp`.
-Добавьте флаг `-n 1` для более стабильного подключения в 1 поток (ограничение 5 Мбит/с для ВК)
+Добавьте флаг `-n 1` для более стабильного подключения в 1 поток (ограничение 5 МБит/с для ВК)
## Яндекс телемост
@@ -233,7 +318,7 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down
## v2ray
-Вместо WireGuard можно использовать любое V2Ray-ядро которое его поддерживает (например, xray или sing-box) и любой V2Ray-клиент который использует это ядро (например, v2rayN или v2rayNG). С помощью их вы сможете добавить больше входящих интерфейсов (например, SOCKS) и реализовать точечный роутинг.
+Вместо WireGuard можно использовать любое V2Ray-ядро, которое его поддерживает (например, xray или sing-box) и любой V2Ray-клиент, который использует это ядро (например, v2rayN или v2rayNG). С помощью их вы сможете добавить больше входящих интерфейсов (например, SOCKS) и реализовать точечный роутинг.
Пример конфигов:
diff --git a/client/main.go b/client/main.go
index 07a2be4..a3f7570 100644
--- a/client/main.go
+++ b/client/main.go
@@ -40,7 +40,7 @@ import (
"github.com/pion/turn/v5"
)
-type getCredsFunc func(string) (string, string, string, error)
+type getCredsFunc func(ctx context.Context, link string, streamID int) (string, string, string, error)
type directNet struct{}
@@ -140,23 +140,39 @@ func (l directTCPListener) AcceptTCP() (transport.TCPConn, error) {
return l.TCPListener.AcceptTCP()
}
-// region automatic captcha solver
+// region Helper: HTTP Headers Injection
-type vkCaptchaError struct {
- ErrorCode int
- ErrorMsg string
- CaptchaSid string
- RedirectUri string
- SessionToken string
- CaptchaTs string
- CaptchaAttempt string
+// applyBrowserProfile applies consistent User-Agent and Client Hints to bypass WAFs
+func applyBrowserProfile(req *http.Request, profile Profile) {
+ req.Header.Set("User-Agent", profile.UserAgent)
+ req.Header.Set("sec-ch-ua", profile.SecChUa)
+ req.Header.Set("sec-ch-ua-mobile", profile.SecChUaMobile)
+ req.Header.Set("sec-ch-ua-platform", profile.SecChUaPlatform)
+ req.Header.Set("Accept-Language", "en-US,en;q=0.9")
+ req.Header.Set("DNT", "1")
}
-func parseVkCaptchaError(errData map[string]interface{}) *vkCaptchaError {
+// endregion
+
+// region Automatic Captcha Solver & Authentication
+
+type VkCaptchaError struct {
+ ErrorCode int
+ ErrorMsg string
+ CaptchaSid string
+ CaptchaImg string
+ RedirectUri string
+ IsSoundCaptchaAvailable bool
+ SessionToken string
+ CaptchaTs string
+ CaptchaAttempt string
+}
+
+func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError {
codeFloat, _ := errData["error_code"].(float64)
- redirectUri, _ := errData["redirect_uri"].(string)
- errorMsg, _ := errData["error_msg"].(string)
+ code := int(codeFloat)
+ redirectUri, _ := errData["redirect_uri"].(string)
captchaSid, _ := errData["captcha_sid"].(string)
if captchaSid == "" {
if sidNum, ok := errData["captcha_sid"].(float64); ok {
@@ -164,6 +180,9 @@ func parseVkCaptchaError(errData map[string]interface{}) *vkCaptchaError {
}
}
+ captchaImg, _ := errData["captcha_img"].(string)
+ errorMsg, _ := errData["error_msg"].(string)
+
var sessionToken string
if redirectUri != "" {
if parsed, err := neturl.Parse(redirectUri); err == nil {
@@ -171,6 +190,8 @@ func parseVkCaptchaError(errData map[string]interface{}) *vkCaptchaError {
}
}
+ isSound, _ := errData["is_sound_captcha_available"].(bool)
+
var captchaTs string
if tsFloat, ok := errData["captcha_ts"].(float64); ok {
captchaTs = fmt.Sprintf("%.0f", tsFloat)
@@ -185,69 +206,111 @@ func parseVkCaptchaError(errData map[string]interface{}) *vkCaptchaError {
captchaAttempt = attStr
}
- return &vkCaptchaError{
- ErrorCode: int(codeFloat),
- ErrorMsg: errorMsg,
- CaptchaSid: captchaSid,
- RedirectUri: redirectUri,
- SessionToken: sessionToken,
- CaptchaTs: captchaTs,
- CaptchaAttempt: captchaAttempt,
+ return &VkCaptchaError{
+ ErrorCode: code,
+ ErrorMsg: errorMsg,
+ CaptchaSid: captchaSid,
+ CaptchaImg: captchaImg,
+ RedirectUri: redirectUri,
+ IsSoundCaptchaAvailable: isSound,
+ SessionToken: sessionToken,
+ CaptchaTs: captchaTs,
+ CaptchaAttempt: captchaAttempt,
}
}
-func solveVkCaptcha(ctx context.Context, captchaErr *vkCaptchaError, dialer *dnsdialer.Dialer) (string, error) {
- log.Printf("Solving VK Smart Captcha automatically...")
+func (e *VkCaptchaError) IsCaptchaError() bool {
+ return e.ErrorCode == 14
+}
+
+func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, dialer *dnsdialer.Dialer, profile Profile) (string, error) {
+ log.Printf("[STREAM %d] [Captcha] Solving Not Robot Captcha...", streamID)
+
if captchaErr.SessionToken == "" {
- return "", fmt.Errorf("no session_token in redirect_uri")
+ return "", fmt.Errorf("no session_token in redirect_uri for auto-solve")
+ }
+ if captchaErr.RedirectUri == "" {
+ return "", fmt.Errorf("no redirect_uri for auto-solve")
}
- powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectUri, dialer)
+ // HAR Timing: Browser page load & user perception
+ time.Sleep(1500*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond)
+
+ powInput, difficulty, cookies, err := fetchPowInput(ctx, captchaErr.RedirectUri, streamID, dialer, profile)
if err != nil {
return "", fmt.Errorf("failed to fetch PoW input: %w", err)
}
+ log.Printf("[STREAM %d] [Captcha] PoW input: %s, difficulty: %d", streamID, powInput, difficulty)
+
hash := solvePoW(powInput, difficulty)
+ log.Printf("[STREAM %d] [Captcha] PoW solved: hash=%s", streamID, hash)
- successToken, err := callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, dialer)
+ successToken, err := callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, cookies, streamID, dialer, profile)
if err != nil {
return "", fmt.Errorf("captchaNotRobot API failed: %w", err)
}
- log.Printf("VK Smart Captcha Solved Successfully!")
+ log.Printf("[STREAM %d] [Captcha] Success! Got success_token", streamID)
return successToken, nil
}
-func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Dialer) (string, int, error) {
+func fetchPowInput(ctx context.Context, redirectUri string, streamID int, dialer *dnsdialer.Dialer, profile Profile) (string, int, string, error) {
+ parsedURL, err := neturl.Parse(redirectUri)
+ if err != nil {
+ return "", 0, "", err
+ }
+ domain := parsedURL.Hostname()
+
req, err := http.NewRequestWithContext(ctx, "GET", redirectUri, nil)
if err != nil {
- return "", 0, err
+ return "", 0, "", err
}
- req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36")
+
+ req.Host = domain // Explicitly force Host header
+ applyBrowserProfile(req, profile)
+ req.Header.Set("Sec-Fetch-Site", "none")
+ req.Header.Set("Sec-Fetch-Mode", "navigate")
+ req.Header.Set("Sec-Fetch-Dest", "document")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
DialContext: dialer.DialContext,
+ TLSClientConfig: &tls.Config{
+ ServerName: domain, // Force SNI for DPI evasion
+ },
},
}
resp, err := client.Do(req)
if err != nil {
- return "", 0, err
+ return "", 0, "", err
+ }
+ defer func(Body io.ReadCloser) {
+ _ = Body.Close()
+ }(resp.Body)
+
+ var cookies []string
+ for _, setCookie := range resp.Header.Values("Set-Cookie") {
+ cookieParts := strings.Split(setCookie, ";")
+ cookies = append(cookies, strings.TrimSpace(cookieParts[0]))
+ }
+ cookieHeader := strings.Join(cookies, "; ")
+ if cookieHeader != "" {
+ log.Printf("[STREAM %d] [Captcha] Captcha page set %d cookie(s)", streamID, len(cookies))
}
- defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
- return "", 0, err
+ return "", 0, "", err
}
html := string(body)
powInputRe := regexp.MustCompile(`const\s+powInput\s*=\s*"([^"]+)"`)
powInputMatch := powInputRe.FindStringSubmatch(html)
if len(powInputMatch) < 2 {
- return "", 0, fmt.Errorf("powInput not found in captcha HTML")
+ return "", 0, "", fmt.Errorf("powInput not found in captcha HTML")
}
powInput := powInputMatch[1]
@@ -259,7 +322,7 @@ func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Di
difficulty = d
}
}
- return powInput, difficulty, nil
+ return powInput, difficulty, cookieHeader, nil
}
func solvePoW(powInput string, difficulty int) string {
@@ -275,22 +338,39 @@ func solvePoW(powInput string, difficulty int) string {
return ""
}
-func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer *dnsdialer.Dialer) (string, error) {
+func callCaptchaNotRobot(ctx context.Context, sessionToken, hash, cookies string, streamID int, dialer *dnsdialer.Dialer, profile Profile) (string, error) {
vkReq := func(method string, postData string) (map[string]interface{}, error) {
reqURL := "https://api.vk.ru/method/" + method + "?v=5.131"
+ parsedURL, _ := neturl.Parse(reqURL)
+ domain := parsedURL.Hostname()
+
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(postData))
if err != nil {
return nil, err
}
- req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36")
+
+ req.Host = domain
+ applyBrowserProfile(req, profile)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Set("Origin", "https://vk.ru")
- req.Header.Set("Referer", "https://vk.ru/")
+ req.Header.Set("Accept", "*/*")
+ req.Header.Set("Origin", "https://id.vk.ru")
+ req.Header.Set("Referer", "https://id.vk.ru/")
+ req.Header.Set("Sec-Fetch-Site", "same-site")
+ req.Header.Set("Sec-Fetch-Mode", "cors")
+ req.Header.Set("Sec-Fetch-Dest", "empty")
+ req.Header.Set("Priority", "u=1, i")
+
+ if cookies != "" {
+ req.Header.Set("Cookie", cookies)
+ }
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
DialContext: dialer.DialContext,
+ TLSClientConfig: &tls.Config{
+ ServerName: domain, // Enforce SNI for DPI evasion
+ },
},
}
@@ -298,7 +378,9 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer
if err != nil {
return nil, err
}
- defer httpResp.Body.Close()
+ defer func(Body io.ReadCloser) {
+ _ = Body.Close()
+ }(httpResp.Body)
body, err := io.ReadAll(httpResp.Body)
if err != nil {
@@ -313,32 +395,40 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer
baseParams := fmt.Sprintf("session_token=%s&domain=vk.com&adFp=&access_token=", neturl.QueryEscape(sessionToken))
- // Step 1: settings
+ log.Printf("[STREAM %d] [Captcha] Step 1/4: settings", streamID)
if _, err := vkReq("captchaNotRobot.settings", baseParams); err != nil {
return "", fmt.Errorf("settings failed: %w", err)
}
- time.Sleep(200 * time.Millisecond)
- // Step 2: componentDone
- browserFp := fmt.Sprintf("%032x", rand.Int63())
+ // HAR Timing: settings -> componentDone
+ time.Sleep(100*time.Millisecond + time.Duration(rand.Intn(100))*time.Millisecond)
+
+ log.Printf("[STREAM %d] [Captcha] Step 2/4: componentDone", streamID)
+ browserFp := fmt.Sprintf("%016x%016x", rand.Int63(), rand.Int63())
deviceJSON := `{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1032,"innerWidth":1920,"innerHeight":945,"devicePixelRatio":1,"language":"en-US","languages":["en-US"],"webdriver":false,"hardwareConcurrency":16,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"denied"}`
componentDoneData := baseParams + fmt.Sprintf("&browser_fp=%s&device=%s", browserFp, neturl.QueryEscape(deviceJSON))
if _, err := vkReq("captchaNotRobot.componentDone", componentDoneData); err != nil {
return "", fmt.Errorf("componentDone failed: %w", err)
}
- time.Sleep(200 * time.Millisecond)
- // Step 3: check
+ // HAR Timing: componentDone -> check ≈ 1.95s + statEvents delay ≈ 3.2s total
+ time.Sleep(1950*time.Millisecond + time.Duration(rand.Intn(1250))*time.Millisecond)
+
+ log.Printf("[STREAM %d] [Captcha] Step 3/4: check", streamID)
cursorJSON := `[{"x":950,"y":500},{"x":945,"y":510},{"x":940,"y":520},{"x":938,"y":525},{"x":938,"y":525}]`
answer := base64.StdEncoding.EncodeToString([]byte("{}"))
debugInfo := "d44f534ce8deb56ba20be52e05c433309b49ee4d2a70602deeb17a1954257785"
+ baseDownlink := 8.0 + rand.Float64()*4.0
+ downlinkStr := fmt.Sprintf("%.1f", baseDownlink)
+ connectionDownlink := "[" + downlinkStr + "," + downlinkStr + "," + downlinkStr + "," + downlinkStr + "," + downlinkStr + "," + downlinkStr + "," + downlinkStr + "]"
+
checkData := baseParams + fmt.Sprintf(
"&accelerometer=%s&gyroscope=%s&motion=%s&cursor=%s&taps=%s&connectionRtt=%s&connectionDownlink=%s&browser_fp=%s&hash=%s&answer=%s&debug_info=%s",
neturl.QueryEscape("[]"), neturl.QueryEscape("[]"), neturl.QueryEscape("[]"),
neturl.QueryEscape(cursorJSON), neturl.QueryEscape("[]"), neturl.QueryEscape("[]"),
- neturl.QueryEscape("[9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5]"),
+ neturl.QueryEscape(connectionDownlink),
browserFp, hash, answer, debugInfo,
)
@@ -360,24 +450,232 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer
return "", fmt.Errorf("success_token not found")
}
- time.Sleep(200 * time.Millisecond)
-
- // Step 4: endSession
- vkReq("captchaNotRobot.endSession", baseParams)
+ log.Printf("[STREAM %d] [Captcha] Step 4/4: endSession", streamID)
+ _, err = vkReq("captchaNotRobot.endSession", baseParams)
+ if err != nil {
+ log.Printf("[STREAM %d] [Captcha] Warning: endSession failed: %v", streamID, err)
+ }
return successToken, nil
}
-// endregion automatic captcha solver
+// endregion
+
+// region VK Credentials & Caching Layer
+
+type VKCredentials struct {
+ ClientID string
+ ClientSecret string
+}
+
+var vkCredentialsList = []VKCredentials{
+ {ClientID: "6287487", ClientSecret: "QbYic1K3lEV5kTGiqlq2"}, // VK_WEB_APP_ID
+ {ClientID: "7879029", ClientSecret: "aR5NKGmm03GYrCiNKsaw"}, // VK_MVK_APP_ID
+ {ClientID: "52461373", ClientSecret: "o557NLIkAErNhakXrQ7A"}, // VK_WEB_VKVIDEO_APP_ID
+ {ClientID: "52649896", ClientSecret: "WStp4ihWG4l3nmXZgIbC"}, // VK_MVK_VKVIDEO_APP_ID
+ {ClientID: "51781872", ClientSecret: "IjjCNl4L4Tf5QZEXIHKK"}, // VK_ID_AUTH_APP
+}
+
+type TurnCredentials struct {
+ Username string
+ Password string
+ ServerAddr string
+ ExpiresAt time.Time
+ Link string
+}
+
+type StreamCredentialsCache struct {
+ creds TurnCredentials
+ mutex sync.RWMutex
+ errorCount atomic.Int32
+ lastErrorTime atomic.Int64
+}
+
+const (
+ credentialLifetime = 10 * time.Minute
+ cacheSafetyMargin = 60 * time.Second
+ maxCacheErrors = 3
+ errorWindow = 10 * time.Second
+ streamsPerCache = 10
+)
+
+func getCacheID(streamID int) int {
+ return streamID / streamsPerCache
+}
+
+var vkRequestMu sync.Mutex
+
+func vkDelayRandom(minMs, maxMs int) {
+ ms := minMs + rand.Intn(maxMs-minMs+1)
+ time.Sleep(time.Duration(ms) * time.Millisecond)
+}
+
+var credentialsStore = struct {
+ mu sync.RWMutex
+ caches map[int]*StreamCredentialsCache
+}{
+ caches: make(map[int]*StreamCredentialsCache),
+}
+
+func getStreamCache(streamID int) *StreamCredentialsCache {
+ cacheID := getCacheID(streamID)
+
+ credentialsStore.mu.RLock()
+ cache, exists := credentialsStore.caches[cacheID]
+ credentialsStore.mu.RUnlock()
+
+ if exists {
+ return cache
+ }
+
+ credentialsStore.mu.Lock()
+ defer credentialsStore.mu.Unlock()
+
+ if cache, exists = credentialsStore.caches[cacheID]; exists {
+ return cache
+ }
+
+ cache = &StreamCredentialsCache{}
+ credentialsStore.caches[cacheID] = cache
+ return cache
+}
+
+func isAuthError(err error) bool {
+ if err == nil {
+ return false
+ }
+ errStr := err.Error()
+ return strings.Contains(errStr, "401") ||
+ strings.Contains(errStr, "Unauthorized") ||
+ strings.Contains(errStr, "authentication") ||
+ strings.Contains(errStr, "invalid credential") ||
+ strings.Contains(errStr, "stale nonce")
+}
+
+func handleAuthError(streamID int) bool {
+ cache := getStreamCache(streamID)
+ cacheID := getCacheID(streamID)
+
+ now := time.Now().Unix()
+
+ if now-cache.lastErrorTime.Load() > int64(errorWindow.Seconds()) {
+ cache.errorCount.Store(0)
+ }
+
+ count := cache.errorCount.Add(1)
+ cache.lastErrorTime.Store(now)
+
+ log.Printf("[STREAM %d] Auth error (cache=%d, count=%d/%d)", streamID, cacheID, count, maxCacheErrors)
+
+ if count >= maxCacheErrors {
+ log.Printf("[VK Auth] Multiple auth errors detected (%d), invalidating cache %d for stream %d...", count, cacheID, streamID)
+ cache.invalidate(streamID)
+ return true
+ }
+ return false
+}
+
+func (c *StreamCredentialsCache) invalidate(streamID int) {
+ c.mutex.Lock()
+ c.creds = TurnCredentials{}
+ c.mutex.Unlock()
+
+ c.errorCount.Store(0)
+ c.lastErrorTime.Store(0)
+
+ log.Printf("[STREAM %d] [VK Auth] Credentials cache invalidated", streamID)
+}
+
+func getVkCredsCached(ctx context.Context, link string, streamID int, dialer *dnsdialer.Dialer) (string, string, string, error) {
+ cache := getStreamCache(streamID)
+ cacheID := getCacheID(streamID)
+
+ cache.mutex.RLock()
+ if cache.creds.Link == link && time.Now().Before(cache.creds.ExpiresAt) {
+ expires := time.Until(cache.creds.ExpiresAt)
+ u, p, a := cache.creds.Username, cache.creds.Password, cache.creds.ServerAddr
+ cache.mutex.RUnlock()
+ log.Printf("[STREAM %d] [VK Auth] Using cached credentials (cache=%d, expires in %v)", streamID, cacheID, expires)
+ return u, p, a, nil
+ }
+ cache.mutex.RUnlock()
+
+ cache.mutex.Lock()
+ defer cache.mutex.Unlock()
+
+ // Double-check
+ if cache.creds.Link == link && time.Now().Before(cache.creds.ExpiresAt) {
+ expires := time.Until(cache.creds.ExpiresAt)
+ log.Printf("[STREAM %d] [VK Auth] Using cached credentials (cache=%d, expires in %v)", streamID, cacheID, expires)
+ return cache.creds.Username, cache.creds.Password, cache.creds.ServerAddr, nil
+ }
-func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, error) {
+ log.Printf("[STREAM %d] [VK Auth] Cache miss (cache=%d), starting credential fetch...", streamID, cacheID)
+
+ select {
+ case <-ctx.Done():
+ return "", "", "", ctx.Err()
+ default:
+ }
+
+ user, pass, addr, err := fetchVkCredsSerialized(ctx, link, streamID, dialer)
+ if err != nil {
+ return "", "", "", err
+ }
+
+ cache.creds = TurnCredentials{
+ Username: user,
+ Password: pass,
+ ServerAddr: addr,
+ ExpiresAt: time.Now().Add(credentialLifetime - cacheSafetyMargin),
+ Link: link,
+ }
+
+ log.Printf("[STREAM %d] [VK Auth] Success! Credentials cached until %v (cache=%d)", streamID, cache.creds.ExpiresAt, cacheID)
+ return user, pass, addr, nil
+}
+
+func fetchVkCredsSerialized(ctx context.Context, link string, streamID int, dialer *dnsdialer.Dialer) (string, string, string, error) {
+ vkRequestMu.Lock()
+ defer vkRequestMu.Unlock()
+ return fetchVkCreds(ctx, link, streamID, dialer)
+}
+
+func fetchVkCreds(ctx context.Context, link string, streamID int, dialer *dnsdialer.Dialer) (string, string, string, error) {
+ var lastErr error
+
+ for _, creds := range vkCredentialsList {
+ log.Printf("[STREAM %d] [VK Auth] Trying credentials: client_id=%s", streamID, creds.ClientID)
+
+ user, pass, addr, err := getTokenChain(ctx, link, streamID, creds, dialer)
+
+ if err == nil {
+ log.Printf("[STREAM %d] [VK Auth] Success with client_id=%s", streamID, creds.ClientID)
+ return user, pass, addr, nil
+ }
+
+ lastErr = err
+ log.Printf("[STREAM %d] [VK Auth] Failed with client_id=%s: %v", streamID, creds.ClientID, err)
+
+ if strings.Contains(err.Error(), "error_code:29") || strings.Contains(err.Error(), "error_code: 29") || strings.Contains(err.Error(), "Rate limit") {
+ log.Printf("[STREAM %d] [VK Auth] Rate limit detected, trying next credentials...", streamID)
+ }
+ }
+
+ return "", "", "", fmt.Errorf("all VK credentials failed: %w", lastErr)
+}
+
+func getTokenChain(ctx context.Context, link string, streamID int, creds VKCredentials, dialer *dnsdialer.Dialer) (string, string, string, error) {
profile := getRandomProfile()
name := generateName()
escapedName := neturl.QueryEscape(name)
- log.Printf("Connecting Identity - Name: %s | User-Agent: %s", name, profile.UserAgent)
+ log.Printf("[STREAM %d] [VK Auth] Connecting Identity - Name: %s | User-Agent: %s", streamID, name, profile.UserAgent)
doRequest := func(data string, url string) (resp map[string]interface{}, err error) {
+ parsedURL, _ := neturl.Parse(url)
+ domain := parsedURL.Hostname()
+
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
@@ -385,17 +683,28 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
DialContext: dialer.DialContext,
+ TLSClientConfig: &tls.Config{
+ ServerName: domain, // Force SNI for DPI evasion
+ },
},
}
defer client.CloseIdleConnections()
- req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
+ req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer([]byte(data)))
if err != nil {
return nil, err
}
- req.Header.Add("User-Agent", profile.UserAgent)
- req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Host = domain
+ applyBrowserProfile(req, profile)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("Accept", "*/*")
+ req.Header.Set("Origin", "https://vk.ru")
+ req.Header.Set("Referer", "https://vk.ru/")
+ req.Header.Set("Sec-Fetch-Site", "same-site")
+ req.Header.Set("Sec-Fetch-Mode", "cors")
+ req.Header.Set("Sec-Fetch-Dest", "empty")
+ req.Header.Set("Priority", "u=1, i")
httpResp, err := client.Do(req)
if err != nil {
@@ -416,25 +725,15 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string,
if err != nil {
return nil, err
}
-
return resp, nil
}
- var resp map[string]interface{}
- defer func() {
- if r := recover(); r != nil {
- log.Panicf("get TURN creds error: %v\n\n", resp)
- }
- }()
-
- data := "client_id=6287487&token_type=messages&client_secret=QbYic1K3lEV5kTGiqlq2&version=1&app_id=6287487"
- url := "https://login.vk.ru/?act=get_anonym_token"
-
- resp, err := doRequest(data, url)
+ // Token 1
+ data := fmt.Sprintf("client_id=%s&token_type=messages&client_secret=%s&version=1&app_id=%s", creds.ClientID, creds.ClientSecret, creds.ClientID)
+ resp, err := doRequest(data, "https://login.vk.ru/?act=get_anonym_token")
if err != nil {
- return "", "", "", fmt.Errorf("request error:%s", err)
+ return "", "", "", err
}
-
dataMap, ok := resp["data"].(map[string]interface{})
if !ok {
return "", "", "", fmt.Errorf("unexpected anon token response: %v", resp)
@@ -444,42 +743,76 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string,
return "", "", "", fmt.Errorf("missing access_token in response: %v", resp)
}
+ vkDelayRandom(100, 200)
+
+ // Token 1 -> getCallPreview
+ data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&fields=photo_200&access_token=%s", link, token1)
+ _, _ = doRequest(data, "https://api.vk.ru/method/calls.getCallPreview?v=5.275&client_id="+creds.ClientID)
+
+ vkDelayRandom(500, 1000) // HAR: Delay updated
+
+ // Token 2
data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&access_token=%s", link, escapedName, token1)
- url = "https://api.vk.ru/method/calls.getAnonymousToken?v=5.274&client_id=6287487"
+ urlAddr := fmt.Sprintf("https://api.vk.ru/method/calls.getAnonymousToken?v=5.275&client_id=%s", creds.ClientID)
var token2 string
const maxCaptchaAttempts = 3
for attempt := 0; attempt <= maxCaptchaAttempts; attempt++ {
- resp, err = doRequest(data, url)
+ resp, err = doRequest(data, urlAddr)
if err != nil {
- return "", "", "", fmt.Errorf("request error:%s", err)
+ return "", "", "", err
}
- // Check for captcha error
if errObj, hasErr := resp["error"].(map[string]interface{}); hasErr {
- errCode, _ := errObj["error_code"].(float64)
- if errCode == 14 {
- if attempt == maxCaptchaAttempts {
- return "", "", "", fmt.Errorf("captcha failed after %d attempts", maxCaptchaAttempts)
- }
-
- captchaErr := parseVkCaptchaError(errObj)
- if captchaErr.SessionToken != "" {
- successToken, solveErr := solveVkCaptcha(context.Background(), captchaErr, dialer)
+ captchaErr := ParseVkCaptchaError(errObj)
+ if captchaErr != nil && captchaErr.IsCaptchaError() {
+ var successToken string
+ var captchaKey string
+ var solveErr error
+
+ // Try automatic if possible and attempts remain
+ if attempt < maxCaptchaAttempts && captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" {
+ successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, dialer, profile)
if solveErr != nil {
- return "", "", "", fmt.Errorf("auto captcha solve error: %w", solveErr)
+ log.Printf("[STREAM %d] [Captcha] Auto solve failed: %v. Falling back to manual...", streamID, solveErr)
}
+ } else if attempt >= maxCaptchaAttempts {
+ log.Printf("[STREAM %d] [Captcha] Max auto attempts reached. Falling back to manual...", streamID)
+ solveErr = fmt.Errorf("max attempts reached")
+ } else {
+ log.Printf("[STREAM %d] [Captcha] Missing fields for auto solve. Falling back to manual...", streamID)
+ solveErr = fmt.Errorf("missing fields")
+ }
- if captchaErr.CaptchaAttempt == "0" || captchaErr.CaptchaAttempt == "" {
- captchaErr.CaptchaAttempt = "1"
+ // If auto failed, or we skipped it, drop to manual fallback
+ if solveErr != nil {
+ if captchaErr.RedirectUri != "" {
+ successToken, solveErr = solveCaptchaViaProxy(captchaErr.RedirectUri, dialer)
+ if solveErr != nil {
+ return "", "", "", fmt.Errorf("manual proxy captcha solve error: %w", solveErr)
+ }
+ } else if captchaErr.CaptchaImg != "" {
+ captchaKey, solveErr = solveCaptchaViaHTTP(captchaErr.CaptchaImg)
+ if solveErr != nil {
+ return "", "", "", fmt.Errorf("manual HTTP captcha solve error: %w", solveErr)
+ }
+ } else {
+ return "", "", "", fmt.Errorf("cannot solve captcha manually: no redirect_uri or captcha_img")
}
+ }
- data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&access_token=%s&captcha_key=&captcha_sid=%s&is_sound_captcha=0&success_token=%s&captcha_ts=%s&captcha_attempt=%s",
- link, escapedName, token1, captchaErr.CaptchaSid, neturl.QueryEscape(successToken), captchaErr.CaptchaTs, captchaErr.CaptchaAttempt)
- continue
+ if captchaErr.CaptchaAttempt == "0" || captchaErr.CaptchaAttempt == "" {
+ captchaErr.CaptchaAttempt = "1"
+ }
+
+ if captchaKey != "" {
+ data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&captcha_key=%s&captcha_sid=%s&access_token=%s",
+ link, escapedName, neturl.QueryEscape(captchaKey), captchaErr.CaptchaSid, token1)
} else {
- return "", "", "", fmt.Errorf("old image captcha detected - not supported in auto solver")
+ data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&captcha_key=&captcha_sid=%s&is_sound_captcha=0&success_token=%s&captcha_ts=%s&captcha_attempt=%s&access_token=%s",
+ link, escapedName, captchaErr.CaptchaSid, neturl.QueryEscape(successToken), captchaErr.CaptchaTs, captchaErr.CaptchaAttempt, token1)
}
+ continue
}
return "", "", "", fmt.Errorf("VK API error: %v", errObj)
}
@@ -495,40 +828,48 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string,
break
}
- data = fmt.Sprintf("%s%s%s", "session_data=%7B%22version%22%3A2%2C%22device_id%22%3A%22", uuid.New(), "%22%2C%22client_version%22%3A1.1%2C%22client_type%22%3A%22SDK_JS%22%7D&method=auth.anonymLogin&format=JSON&application_key=CGMMEJLGDIHBABABA")
- url = "https://calls.okcdn.ru/fb.do"
+ vkDelayRandom(100, 200)
- resp, err = doRequest(data, url)
+ // Token 3
+ sessionData := fmt.Sprintf(`{"version":2,"device_id":"%s","client_version":1.1,"client_type":"SDK_JS"}`, uuid.New())
+ data = fmt.Sprintf("session_data=%s&method=auth.anonymLogin&format=JSON&application_key=CGMMEJLGDIHBABABA", neturl.QueryEscape(sessionData))
+ resp, err = doRequest(data, "https://calls.okcdn.ru/fb.do")
if err != nil {
- return "", "", "", fmt.Errorf("request error:%s", err)
+ return "", "", "", err
}
-
token3 := resp["session_key"].(string)
- data = fmt.Sprintf("joinLink=%s&isVideo=false&protocolVersion=5&anonymToken=%s&method=vchat.joinConversationByLink&format=JSON&application_key=CGMMEJLGDIHBABABA&session_key=%s", link, token2, token3)
- url = "https://calls.okcdn.ru/fb.do"
+ vkDelayRandom(100, 200)
- resp, err = doRequest(data, url)
+ // Token 4 -> TURN Creds
+ data = fmt.Sprintf("joinLink=%s&isVideo=false&protocolVersion=5&capabilities=2F7F&anonymToken=%s&method=vchat.joinConversationByLink&format=JSON&application_key=CGMMEJLGDIHBABABA&session_key=%s", link, token2, token3)
+ resp, err = doRequest(data, "https://calls.okcdn.ru/fb.do")
if err != nil {
- return "", "", "", fmt.Errorf("request error:%s", err)
+ return "", "", "", err
}
- user := resp["turn_server"].(map[string]interface{})["username"].(string)
- pass := resp["turn_server"].(map[string]interface{})["credential"].(string)
- turn := resp["turn_server"].(map[string]interface{})["urls"].([]interface{})[0].(string)
+ ts := resp["turn_server"].(map[string]interface{})
+ user := ts["username"].(string)
+ pass := ts["credential"].(string)
+ urls := ts["urls"].([]interface{})
+ urlStr := urls[0].(string)
- clean := strings.Split(turn, "?")[0]
+ clean := strings.Split(urlStr, "?")[0]
address := strings.TrimPrefix(strings.TrimPrefix(clean, "turn:"), "turns:")
+ vkDelayRandom(4000, 5000) // HAR: Final matching delay
+
return user, pass, address, nil
}
+// endregion
+
func getYandexCreds(link string) (string, string, string, error) {
const debug = false
const telemostConfHost = "cloud-api.yandex.ru"
telemostConfPath := fmt.Sprintf("%s%s%s", "/telemost_front/v2/telemost/conferences/https%3A%2F%2Ftelemost.yandex.ru%2Fj%2F", link, "/connection?next_gen_media_platform_allowed=false")
+
profile := getRandomProfile()
- userAgent := profile.UserAgent
name := generateName()
type ConferenceResponse struct {
@@ -657,7 +998,8 @@ func getYandexCreds(link string) (string, string, string, error) {
if err != nil {
return "", "", "", err
}
- req.Header.Set("User-Agent", userAgent)
+
+ applyBrowserProfile(req, profile)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", "https://telemost.yandex.ru/")
req.Header.Set("Origin", "https://telemost.yandex.ru")
@@ -689,7 +1031,7 @@ func getYandexCreds(link string) (string, string, string, error) {
}
h := http.Header{}
h.Set("Origin", "https://telemost.yandex.ru")
- h.Set("User-Agent", userAgent)
+ h.Set("User-Agent", profile.UserAgent)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
@@ -731,7 +1073,7 @@ func getYandexCreds(link string) (string, string, string, error) {
SdkInfo: SdkInfo{
Implementation: "browser",
Version: "5.15.0",
- UserAgent: userAgent,
+ UserAgent: profile.UserAgent,
HwConcurrency: 4,
},
SdkInitializationID: uuid.New().String(),
@@ -874,7 +1216,6 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
}()
log.Printf("Established DTLS connection!\n")
- // Trigger the okchan safely to spawn the rest of the threads
if okchan != nil {
go func() {
select {
@@ -895,7 +1236,6 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
}
})
- // Start read-loop on listenConn
go func() {
defer wg.Done()
defer dtlscancel()
@@ -912,7 +1252,7 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
return
}
- globalClientWGAddr.Store(addr1) // store local WG peer address globally
+ globalClientWGAddr.Store(addr1)
_, err1 = dtlsConn.Write(buf[:n])
if err1 != nil {
@@ -922,7 +1262,6 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
}
}()
- // Start read-loop on dtlsConn
go func() {
defer wg.Done()
defer dtlscancel()
@@ -941,7 +1280,6 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
addr1, ok := globalClientWGAddr.Load().(net.Addr)
if !ok {
- // Safely drop packet if wireguard hasn't sent an initial packet yet
continue
}
@@ -978,16 +1316,16 @@ type turnParams struct {
getCreds getCredsFunc
}
-func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, conn2 net.PacketConn, c chan<- error) {
+func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, conn2 net.PacketConn, streamID int, c chan<- error) {
time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond)
var err error = nil
defer func() { c <- err }()
- user, pass, url, err1 := turnParams.getCreds(turnParams.link)
+ user, pass, urlTarget, err1 := turnParams.getCreds(ctx, turnParams.link, streamID)
if err1 != nil {
err = fmt.Errorf("failed to get TURN credentials: %s", err1)
return
}
- urlhost, urlport, err1 := net.SplitHostPort(url)
+ urlhost, urlport, err1 := net.SplitHostPort(urlTarget)
if err1 != nil {
err = fmt.Errorf("failed to parse TURN server address: %s", err1)
return
@@ -1006,8 +1344,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
return
}
turnServerAddr = turnServerUdpAddr.String()
- fmt.Println(turnServerUdpAddr.IP)
- // Dial TURN Server
+
var cfg *turn.ClientConfig
var turnConn net.PacketConn
var d net.Dialer
@@ -1046,8 +1383,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
} else {
addrFamily = turn.RequestedAddressFamilyIPv6
}
- // Start a new TURN Client and wrap our net.Conn in a STUNConn
- // This allows us to simulate datagram based communication over a net.Conn
+
cfg = &turn.ClientConfig{
STUNServerAddr: turnServerAddr,
TURNServerAddr: turnServerAddr,
@@ -1066,18 +1402,17 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
}
defer client.Close()
- // Start listening on the conn provided.
err1 = client.Listen()
if err1 != nil {
err = fmt.Errorf("failed to listen: %s", err1)
return
}
- // Allocate a relay socket on the TURN server. On success, it
- // will return a net.PacketConn which represents the remote
- // socket.
relayConn, err1 := client.Allocate()
if err1 != nil {
+ if isAuthError(err1) {
+ handleAuthError(streamID)
+ }
err = fmt.Errorf("failed to allocate: %s", err1)
return
}
@@ -1087,9 +1422,10 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
}
}()
- // The relayConn's local address is actually the transport
- // address assigned on the TURN server.
- log.Printf("relayed-address=%s", relayConn.LocalAddr().String())
+ // Reset error count on successful allocation
+ getStreamCache(streamID).errorCount.Store(0)
+
+ log.Printf("[STREAM %d] relayed-address=%s", streamID, relayConn.LocalAddr().String())
wg := sync.WaitGroup{}
wg.Add(2)
@@ -1103,7 +1439,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
}
})
var internalPipeAddr atomic.Value
- // Start read-loop on conn2 (output of DTLS)
+
go func() {
defer wg.Done()
defer turncancel()
@@ -1120,7 +1456,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
return
}
- internalPipeAddr.Store(addr1) // store local async pipe peer
+ internalPipeAddr.Store(addr1)
_, err1 = relayConn.WriteTo(buf[:n], peer)
if err1 != nil {
@@ -1130,7 +1466,6 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
}
}()
- // Start read-loop on relayConn
go func() {
defer wg.Done()
defer turncancel()
@@ -1184,84 +1519,27 @@ func oneDtlsConnectionLoop(ctx context.Context, peer *net.UDPAddr, listenConnCha
}
}
-func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, connchan <-chan net.PacketConn, t <-chan time.Time) {
+func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, connchan <-chan net.PacketConn, t <-chan time.Time, streamID int) {
for {
select {
case <-ctx.Done():
return
case conn2 := <-connchan:
- // Ensure we block cleanly until the tick signals to proceed
select {
case <-t:
case <-ctx.Done():
return
}
c := make(chan error)
- go oneTurnConnection(ctx, turnParams, peer, conn2, c)
+ go oneTurnConnection(ctx, turnParams, peer, conn2, streamID, c)
if err := <-c; err != nil {
- log.Printf("%s", err)
+ log.Printf("[STREAM %d] %s", streamID, err)
}
}
}
}
-type turnCred struct {
- user, pass, addr string
-}
-
-// poolCreds allows retrieving unique TURN credentials for N distinct connections.
-// Because it natively handles the automatic captcha bypass, every request gets a unique identity safely.
-func poolCreds(f getCredsFunc, poolSize int) getCredsFunc {
- var mu sync.Mutex
- var pool []turnCred
- var cTime time.Time
- var idx int
-
- return func(link string) (string, string, string, error) {
- mu.Lock()
- defer mu.Unlock()
-
- // Refresh identities every 10 minutes
- if !cTime.IsZero() && time.Since(cTime) > 10*time.Minute {
- pool = nil
- cTime = time.Time{}
- }
-
- if len(pool) < poolSize {
- u, p, a, err := f(link)
- if err == nil {
- pool = append(pool, turnCred{u, p, a})
- cTime = time.Now()
- log.Printf("Successfully registered User Identity %d/%d", len(pool), poolSize)
-
- // Space out requests by 1000ms to avoid API limits
- if len(pool) < poolSize {
- time.Sleep(1000 * time.Millisecond)
- }
-
- c := pool[len(pool)-1]
- idx++
- return c.user, c.pass, c.addr, nil
- }
-
- log.Printf("Failed to get unique TURN identity: %v", err)
- if len(pool) > 0 {
- log.Printf("Falling back to reusing a previous identity...")
- c := pool[idx%len(pool)]
- idx++
- return c.user, c.pass, c.addr, nil
- }
- return "", "", "", err
- }
-
- c := pool[idx%len(pool)]
- idx++
- return c.user, c.pass, c.addr, nil
- }
-}
-
-func main() { //nolint:cyclop
- rand.Seed(time.Now().UnixNano())
+func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
signalChan := make(chan os.Signal, 1)
@@ -1305,13 +1583,13 @@ func main() { //nolint:cyclop
link = parts[len(parts)-1]
dialer := dnsdialer.New(
- dnsdialer.WithResolvers("77.88.8.8:53", "77.88.8.1:53", "8.8.8.8:53", "8.8.4.4:53", "1.1.1.1:53"),
+ dnsdialer.WithResolvers("77.88.8.8:53", "77.88.8.1:53", "8.8.8.8:53", "8.8.4.4:53", "1.1.1.1:53", "1.0.0.1:53"),
dnsdialer.WithStrategy(dnsdialer.Fallback{}),
dnsdialer.WithCache(100, 10*time.Hour, 10*time.Hour),
)
- getCreds = func(s string) (string, string, string, error) {
- return getVkCreds(s, dialer)
+ getCreds = func(ctx context.Context, s string, streamID int) (string, string, string, error) {
+ return getVkCredsCached(ctx, s, streamID, dialer)
}
if *n <= 0 {
*n = 10
@@ -1319,7 +1597,9 @@ func main() { //nolint:cyclop
} else {
parts := strings.Split(*yalink, "j/")
link = parts[len(parts)-1]
- getCreds = getYandexCreds
+ getCreds = func(ctx context.Context, s string, streamID int) (string, string, string, error) {
+ return getYandexCreds(s)
+ }
if *n <= 0 {
*n = 1
}
@@ -1327,12 +1607,13 @@ func main() { //nolint:cyclop
if idx := strings.IndexAny(link, "/?#"); idx != -1 {
link = link[:idx]
}
+
params := &turnParams{
host: *host,
port: *port,
link: link,
udp: *udp,
- getCreds: poolCreds(getCreds, *n),
+ getCreds: getCreds,
}
listenConnChan := make(chan net.PacketConn)
@@ -1360,10 +1641,10 @@ func main() { //nolint:cyclop
if *direct {
for i := 0; i < *n; i++ {
wg1.Add(1)
- go func() {
+ go func(streamID int) {
defer wg1.Done()
- oneTurnConnectionLoop(ctx, params, peer, listenConnChan, t)
- }()
+ oneTurnConnectionLoop(ctx, params, peer, listenConnChan, t, streamID)
+ }(i)
}
} else {
okchan := make(chan struct{})
@@ -1378,7 +1659,7 @@ func main() { //nolint:cyclop
wg1.Add(1)
go func() {
defer wg1.Done()
- oneTurnConnectionLoop(ctx, params, peer, connchan, t)
+ oneTurnConnectionLoop(ctx, params, peer, connchan, t, 0)
}()
select {
@@ -1393,10 +1674,10 @@ func main() { //nolint:cyclop
oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, nil)
}()
wg1.Add(1)
- go func() {
+ go func(streamID int) {
defer wg1.Done()
- oneTurnConnectionLoop(ctx, params, peer, connchan, t)
- }()
+ oneTurnConnectionLoop(ctx, params, peer, connchan, t, streamID)
+ }(i + 1)
}
}
diff --git a/client/manual_captcha.go b/client/manual_captcha.go
new file mode 100644
index 0000000..204240e
--- /dev/null
+++ b/client/manual_captcha.go
@@ -0,0 +1,578 @@
+package main
+
+import (
+ "bytes"
+ "compress/gzip"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ neturl "net/url"
+ "os/exec"
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/bschaatsbergen/dnsdialer"
+)
+
+const captchaListenPort = "8765"
+
+type browserCommand struct {
+ name string
+ args []string
+}
+
+func localCaptchaOrigin() string {
+ return "http://localhost:" + captchaListenPort
+}
+
+func localCaptchaListenAddrs() []string {
+ return []string{
+ "127.0.0.1:" + captchaListenPort,
+ "[::1]:" + captchaListenPort,
+ }
+}
+
+func localCaptchaHosts() []string {
+ return []string{
+ "localhost:" + captchaListenPort,
+ "127.0.0.1:" + captchaListenPort,
+ "[::1]:" + captchaListenPort,
+ }
+}
+
+func isLocalCaptchaHost(host string) bool {
+ for _, localHost := range localCaptchaHosts() {
+ if strings.EqualFold(host, localHost) {
+ return true
+ }
+ }
+ return false
+}
+
+func localCaptchaURLForTarget(targetURL *neturl.URL) string {
+ localURL := &neturl.URL{
+ Scheme: "http",
+ Host: "localhost:" + captchaListenPort,
+ Path: targetURL.Path,
+ RawPath: targetURL.RawPath,
+ RawQuery: targetURL.RawQuery,
+ }
+ if localURL.Path == "" {
+ localURL.Path = "/"
+ }
+ return localURL.String()
+}
+
+func targetOrigin(targetURL *neturl.URL) string {
+ return targetURL.Scheme + "://" + targetURL.Host
+}
+
+func rewriteProxyHeaderURL(raw string, targetURL *neturl.URL) string {
+ if raw == "" {
+ return raw
+ }
+ parsed, err := neturl.Parse(raw)
+ if err != nil {
+ return raw
+ }
+ if parsed.Scheme != "http" || !isLocalCaptchaHost(parsed.Host) {
+ return raw
+ }
+ parsed.Scheme = targetURL.Scheme
+ parsed.Host = targetURL.Host
+ return parsed.String()
+}
+
+func rewriteProxyRequest(req *http.Request, targetURL *neturl.URL) {
+ req.URL.Scheme = targetURL.Scheme
+ req.URL.Host = targetURL.Host
+ if req.URL.Path == "" {
+ req.URL.Path = targetURL.Path
+ }
+ req.Host = targetURL.Host
+
+ req.Header.Del("Accept-Encoding")
+ for _, headerName := range []string{"Origin", "Referer"} {
+ if rewritten := rewriteProxyHeaderURL(req.Header.Get(headerName), targetURL); rewritten != "" {
+ req.Header.Set(headerName, rewritten)
+ } else {
+ req.Header.Del(headerName)
+ }
+ }
+}
+
+func extractSuccessToken(body []byte) string {
+ var payload struct {
+ Response struct {
+ SuccessToken string `json:"success_token"`
+ } `json:"response"`
+ }
+ if err := json.Unmarshal(body, &payload); err != nil {
+ return ""
+ }
+ return payload.Response.SuccessToken
+}
+
+func rewriteProxyCookies(header http.Header) {
+ cookies := (&http.Response{Header: header}).Cookies()
+ if len(cookies) == 0 {
+ return
+ }
+ header.Del("Set-Cookie")
+ for _, cookie := range cookies {
+ cookie.Domain = ""
+ cookie.Secure = false
+ cookie.Partitioned = false
+ if cookie.SameSite == http.SameSiteNoneMode || cookie.SameSite == http.SameSiteStrictMode {
+ cookie.SameSite = http.SameSiteLaxMode
+ }
+ header.Add("Set-Cookie", cookie.String())
+ }
+}
+
+func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string {
+ localOrigin := localCaptchaOrigin()
+ upstreamOrigin := targetOrigin(targetURL)
+ html = strings.ReplaceAll(html, upstreamOrigin, localOrigin)
+
+ script := fmt.Sprintf(`
+
+`, localOrigin, upstreamOrigin)
+
+ switch {
+ case strings.Contains(html, ""):
+ return strings.Replace(html, "", script+"", 1)
+ case strings.Contains(html, "
+Solve the Captcha
+
+"):
+ return strings.Replace(html, "", script+"", 1)
+ default:
+ return html + script
+ }
+}
+
+func newCaptchaProxyTransport(dialer *dnsdialer.Dialer) *http.Transport {
+ transport := &http.Transport{
+ MaxIdleConns: 100,
+ MaxIdleConnsPerHost: 100,
+ IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
+ ForceAttemptHTTP2: true,
+ }
+ if dialer != nil {
+ transport.DialContext = dialer.DialContext
+ }
+ return transport
+}
+
+func startCaptchaServer(srv *http.Server, logPrefix string) error {
+ var listenErrs []string
+ var listening bool
+
+ for _, addr := range localCaptchaListenAddrs() {
+ listener, err := net.Listen("tcp", addr)
+ if err != nil {
+ listenErrs = append(listenErrs, fmt.Sprintf("%s (%v)", addr, err))
+ continue
+ }
+ listening = true
+ go func(listener net.Listener) {
+ if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ log.Printf("%s: %s", logPrefix, err)
+ }
+ }(listener)
+ }
+
+ if listening {
+ return nil
+ }
+
+ return fmt.Errorf("captcha listeners failed: %s", strings.Join(listenErrs, "; "))
+}
+
+// runCaptchaServerAndWait triggers the browser, and waiting gracefully for the solution token.
+func runCaptchaServerAndWait(handler http.Handler, captchaURL string, keyCh <-chan string, logPrefix string) (string, error) {
+ srv := &http.Server{Handler: handler}
+
+ if err := startCaptchaServer(srv, logPrefix); err != nil {
+ return "", err
+ }
+
+ fmt.Println("\n==============================================")
+ fmt.Println("ACTION REQUIRED: MANUAL CAPTCHA SOLVING NEEDED")
+ fmt.Println("Open this URL in your browser: " + captchaURL)
+ fmt.Println("==============================================")
+ fmt.Println()
+ openBrowser(captchaURL)
+
+ key := <-keyCh
+
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+ if err := srv.Shutdown(ctx); err != nil {
+ return "", err
+ }
+
+ return key, nil
+}
+
+// notifyKey pushes the key string to the given channel without blocking
+func notifyKey(keyCh chan<- string, key string) {
+ if key != "" {
+ select {
+ case keyCh <- key:
+ default:
+ }
+ }
+}
+
+func solveCaptchaViaHTTP(captchaImg string) (string, error) {
+ keyCh := make(chan string, 1)
+ mux := http.NewServeMux()
+
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = fmt.Fprintf(w, `
+