diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml
new file mode 100644
index 0000000..2eee1c7
--- /dev/null
+++ b/.github/workflows/issues.yml
@@ -0,0 +1,28 @@
+name: "Close Stale Issues"
+on:
+ schedule:
+ - cron: "0 0 * * 3"
+ workflow_dispatch:
+
+jobs:
+ stale:
+ permissions:
+ issues: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ stale-issue-message: >
+ Привет! 👋
+
+ Данный issue давно не обновлялся. Если в течение недели здесь не будет больше активности, issue будет закрыт.
+
+ Спасибо!
+ close-issue-message: "Issue был закрыт из-за отсутствия активности."
+ days-before-stale: 120
+ days-before-close: 7
+ operations-per-run: 1000
+ ascending: true
+ enable-statistics: true
+ stale-issue-label: "Stale"
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..b7990af 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,28 @@
Только для учебных целей!
## Похожие проекты
-- 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 ВК и Яндекс Телемоста
+- https://github.com/NedgNDG/vk-proxy-auto-installer - автоустановщик VK TURN Proxy (TUI)
+
+#### 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
+
## Настройка
@@ -19,7 +34,7 @@
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 +138,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 +156,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 +176,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 +239,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 +291,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 +319,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..0b5e5eb 100644
--- a/client/main.go
+++ b/client/main.go
@@ -6,6 +6,7 @@ package main
import (
"bytes"
"context"
+ "crypto/md5"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
@@ -29,6 +30,10 @@ import (
"syscall"
"time"
+ fhttp "github.com/bogdanfinn/fhttp"
+ tlsclient "github.com/bogdanfinn/tls-client"
+ "github.com/bogdanfinn/tls-client/profiles"
+
"github.com/bschaatsbergen/dnsdialer"
"github.com/cbeuw/connutil"
"github.com/google/uuid"
@@ -40,7 +45,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{}
@@ -52,15 +57,31 @@ type directListenConfig struct {
*net.ListenConfig
}
-// globalClientWGAddr safely stores the UDP address of the local WireGuard client
-var globalClientWGAddr atomic.Value
+// Global state trackers
+var (
+ activeLocalPeer atomic.Value
+ globalCaptchaLockout atomic.Int64
+ connectedStreams atomic.Int32
+ globalAppCancel context.CancelFunc
+ handshakeSem = make(chan struct{}, 3)
+ isDebug bool
+)
+
+type UDPPacket struct {
+ Data []byte
+ N int
+}
+
+var packetPool = sync.Pool{
+ New: func() any { return &UDPPacket{Data: make([]byte, 2048)} },
+}
func newDirectNet() transport.Net {
return directNet{}
}
func (directNet) ListenPacket(network string, address string) (net.PacketConn, error) {
- return net.ListenPacket(network, address) //nolint:noctx
+ return net.ListenPacket(network, address)
}
func (directNet) ListenUDP(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) {
@@ -77,7 +98,7 @@ func (directNet) ListenTCP(network string, laddr *net.TCPAddr) (transport.TCPLis
}
func (directNet) Dial(network, address string) (net.Conn, error) {
- return net.Dial(network, address) //nolint:noctx
+ return net.Dial(network, address)
}
func (directNet) DialUDP(network string, laddr, raddr *net.UDPAddr) (transport.UDPConn, error) {
@@ -140,23 +161,91 @@ func (l directTCPListener) AcceptTCP() (transport.TCPConn, error) {
return l.TCPListener.AcceptTCP()
}
-// region automatic captcha solver
+// region Helper: HTTP Headers Injection
+
+// 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 applyBrowserProfileFhttp(req *fhttp.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 generateBrowserFp(profile Profile) string {
+ data := profile.UserAgent + profile.SecChUa + "1920x1080x24"
+ h := md5.Sum([]byte(data))
+ return hex.EncodeToString(h[:])
+}
+
+func generateFakeCursor() string {
+ startX := 600 + rand.Intn(400)
+ startY := 300 + rand.Intn(200)
+ startTime := time.Now().UnixMilli() - int64(rand.Intn(2000)+1000)
+ var points []string
+ for i := 0; i < 15+rand.Intn(10); i++ {
+ startX += rand.Intn(15) - 5
+ startY += rand.Intn(15) + 2
+ startTime += int64(rand.Intn(40) + 10)
+ points = append(points, fmt.Sprintf(`{"x":%d,"y":%d,"t":%d}`, startX, startY, startTime))
+ }
+ return "[" + strings.Join(points, ",") + "]"
+}
+
+func getCustomNetDialer() net.Dialer {
+ return net.Dialer{
+ Timeout: 20 * time.Second,
+ KeepAlive: 30 * time.Second,
+ Resolver: &net.Resolver{
+ PreferGo: true,
+ Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
+ var d net.Dialer
+ dnsServers := []string{"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"}
+ var lastErr error
+ for _, dns := range dnsServers {
+ conn, err := d.DialContext(ctx, "udp", dns)
+ if err == nil {
+ return conn, nil
+ }
+ lastErr = err
+ }
+ return nil, lastErr
+ },
+ },
+ }
+}
+
+// endregion
+
+// region Automatic Captcha Solver & Authentication
-type vkCaptchaError struct {
- ErrorCode int
- ErrorMsg string
- CaptchaSid string
- RedirectUri string
- SessionToken string
- CaptchaTs string
- CaptchaAttempt string
+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 {
+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 +253,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 +263,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,58 +279,78 @@ 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 && e.RedirectUri != "" && e.SessionToken != ""
+}
+
+func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, client tlsclient.HttpClient, 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)
+ powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectUri, client, 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, streamID, client, 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) {
- req, err := http.NewRequestWithContext(ctx, "GET", redirectUri, nil)
+func fetchPowInput(ctx context.Context, redirectUri string, client tlsclient.HttpClient, profile Profile) (string, int, error) {
+ parsedURL, err := neturl.Parse(redirectUri)
if err != nil {
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.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
+ domain := parsedURL.Hostname()
- client := &http.Client{
- Timeout: 20 * time.Second,
- Transport: &http.Transport{
- DialContext: dialer.DialContext,
- },
+ req, err := fhttp.NewRequestWithContext(ctx, "GET", redirectUri, nil)
+ if err != nil {
+ return "", 0, err
}
+
+ req.Host = domain
+ applyBrowserProfileFhttp(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")
+
resp, err := client.Do(req)
if err != nil {
return "", 0, err
}
- defer resp.Body.Close()
+ defer func(Body io.ReadCloser) {
+ _ = Body.Close()
+ }(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
@@ -275,30 +389,36 @@ 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 string, streamID int, client tlsclient.HttpClient, profile Profile) (string, error) {
vkReq := func(method string, postData string) (map[string]interface{}, error) {
reqURL := "https://api.vk.ru/method/" + method + "?v=5.131"
- req, err := http.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(postData))
+ parsedURL, _ := neturl.Parse(reqURL)
+ domain := parsedURL.Hostname()
+
+ req, err := fhttp.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.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Set("Origin", "https://vk.ru")
- req.Header.Set("Referer", "https://vk.ru/")
- client := &http.Client{
- Timeout: 20 * time.Second,
- Transport: &http.Transport{
- DialContext: dialer.DialContext,
- },
- }
+ req.Host = domain
+ applyBrowserProfileFhttp(req, profile)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ 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("Sec-GPC", "1")
+ req.Header.Set("Priority", "u=1, i")
httpResp, err := client.Do(req)
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 +433,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())
- 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"}`
+ log.Printf("[STREAM %d] [Captcha] Step 2/4: componentDone", streamID)
+ browserFp := generateBrowserFp(profile)
+ deviceJSON := fmt.Sprintf(`{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1040,"innerWidth":1920,"innerHeight":969,"devicePixelRatio":1,"language":"en-US","languages":["en-US"],"webdriver":false,"hardwareConcurrency":8,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"default","userAgent":"%s","platform":"Win32"}`, profile.UserAgent)
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
- cursorJSON := `[{"x":950,"y":500},{"x":945,"y":510},{"x":940,"y":520},{"x":938,"y":525},{"x":938,"y":525}]`
+ log.Printf("[STREAM %d] [Captcha] Step 3/4: check", streamID)
+ cursorJSON := generateFakeCursor()
answer := base64.StdEncoding.EncodeToString([]byte("{}"))
- debugInfo := "d44f534ce8deb56ba20be52e05c433309b49ee4d2a70602deeb17a1954257785"
+
+ // Dynamically generate debug_info to avoid static fingerprint bans
+ debugInfoBytes := md5.Sum([]byte(profile.UserAgent + strconv.FormatInt(time.Now().UnixNano(), 10)))
+ debugInfo := hex.EncodeToString(debugInfoBytes[:])
+
+ connectionRtt := "[50,50,50,50,50,50,50,50,50,50]"
+ connectionDownlink := "[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]"
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(cursorJSON), neturl.QueryEscape("[]"), neturl.QueryEscape(connectionRtt),
+ neturl.QueryEscape(connectionDownlink),
browserFp, hash, answer, debugInfo,
)
@@ -362,40 +490,280 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer
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 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
+}
+
+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()
+ if isDebug {
+ 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 inside lock
+ if cache.creds.Link == link && time.Now().Before(cache.creds.ExpiresAt) {
+ return cache.creds.Username, cache.creds.Password, cache.creds.ServerAddr, nil
+ }
+
+ 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}
+ return user, pass, addr, nil
+}
+
+var (
+ vkRequestMu sync.Mutex
+ globalLastVkFetchTime time.Time
+)
+
+func fetchVkCredsSerialized(ctx context.Context, link string, streamID int, dialer *dnsdialer.Dialer) (string, string, string, error) {
+ vkRequestMu.Lock()
+ defer vkRequestMu.Unlock()
+
+ // Ensure a minimum cooldown between credential requests to avoid VK rate limits
+ minInterval := 3*time.Second + time.Duration(rand.Intn(3000))*time.Millisecond
+ elapsed := time.Since(globalLastVkFetchTime)
+
+ if !globalLastVkFetchTime.IsZero() && elapsed < minInterval {
+ wait := minInterval - elapsed
+ log.Printf("[STREAM %d] [VK Auth] Throttling: waiting %v to prevent rate limit...", streamID, wait.Truncate(time.Millisecond))
+ select {
+ case <-ctx.Done():
+ return "", "", "", ctx.Err()
+ case <-time.After(wait):
+ }
+ }
+
+ defer func() {
+ globalLastVkFetchTime = time.Now()
+ }()
+
+ return fetchVkCreds(ctx, link, streamID, dialer)
+}
+
+func fetchVkCreds(ctx context.Context, link string, streamID int, dialer *dnsdialer.Dialer) (string, string, string, error) {
+ // Check Global Lockout to prevent API bans
+ if time.Now().Unix() < globalCaptchaLockout.Load() {
+ return "", "", "", fmt.Errorf("CAPTCHA_WAIT_REQUIRED: global lockout active")
+ }
+
+ var lastErr error
+ jar := tlsclient.NewCookieJar()
+
+ 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, jar)
+
+ 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)
+
+ // Hard abort on captcha/fatal conditions instead of trying next creds
+ if strings.Contains(err.Error(), "CAPTCHA_WAIT_REQUIRED") || strings.Contains(err.Error(), "FATAL_CAPTCHA") {
+ return "", "", "", 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, jar tlsclient.CookieJar) (string, string, string, error) {
+ profile := Profile{
+ UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
+ SecChUa: `"Not(A:Brand";v="99", "Google Chrome";v="146", "Chromium";v="146"`,
+ SecChUaMobile: "?0",
+ SecChUaPlatform: `"Windows"`,
+ }
+
+ client, err := tlsclient.NewHttpClient(tlsclient.NewNoopLogger(),
+ tlsclient.WithTimeoutSeconds(20),
+ tlsclient.WithClientProfile(profiles.Chrome_146),
+ tlsclient.WithCookieJar(jar),
+ tlsclient.WithDialer(getCustomNetDialer()),
+ )
+ if err != nil {
+ return "", "", "", fmt.Errorf("failed to initialize tls_client: %w", err)
+ }
-func getVkCreds(link string, 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) {
- client := &http.Client{
- Timeout: 20 * time.Second,
- Transport: &http.Transport{
- MaxIdleConns: 100,
- MaxIdleConnsPerHost: 100,
- IdleConnTimeout: 90 * time.Second,
- DialContext: dialer.DialContext,
- },
- }
- defer client.CloseIdleConnections()
+ parsedURL, _ := neturl.Parse(url)
+ domain := parsedURL.Hostname()
- req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
+ req, err := fhttp.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
+ applyBrowserProfileFhttp(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 +784,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 +802,128 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string,
return "", "", "", fmt.Errorf("missing access_token in response: %v", resp)
}
+ vkDelayRandom(100, 150)
+
+ // 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(200, 400)
+
+ // 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)
+ const maxAutoAttempts = 2
+ for attempt := 0; attempt <= maxAutoAttempts+1; attempt++ {
+ 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 != nil && captchaErr.IsCaptchaError() {
+ var successToken string
+ var captchaKey string
+ var solveErr error
+
+ if attempt < maxAutoAttempts {
+ // Auto Solve Attempts
+ if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" {
+ successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile)
+ if solveErr != nil {
+ log.Printf("[STREAM %d] [Captcha] Auto solve failed: %v", streamID, solveErr)
+ }
+ } else {
+ solveErr = fmt.Errorf("missing fields for auto solve")
+ }
+ } else if attempt == maxAutoAttempts {
+ // Manual Solve Fallback with 60s Timeout
+ log.Printf("[STREAM %d] [Captcha] Auto failed %d times. Triggering MANUAL fallback...", streamID, maxAutoAttempts)
+
+ manualCtx, manualCancel := context.WithTimeout(ctx, 60*time.Second)
+
+ type manualRes struct {
+ token string
+ key string
+ err error
+ }
+ resCh := make(chan manualRes, 1)
+
+ go func() {
+ var t, k string
+ var e error
+ if captchaErr.RedirectUri != "" {
+ t, e = solveCaptchaViaProxy(captchaErr.RedirectUri, dialer)
+ } else if captchaErr.CaptchaImg != "" {
+ k, e = solveCaptchaViaHTTP(captchaErr.CaptchaImg)
+ } else {
+ e = fmt.Errorf("no redirect_uri or captcha_img")
+ }
+ resCh <- manualRes{t, k, e}
+ }()
+
+ select {
+ case res := <-resCh:
+ successToken = res.token
+ captchaKey = res.key
+ solveErr = res.err
+ case <-manualCtx.Done():
+ solveErr = fmt.Errorf("manual captcha timed out after 60s")
+ }
+ manualCancel()
+ } else {
+ solveErr = fmt.Errorf("max attempts reached")
}
- captchaErr := parseVkCaptchaError(errObj)
- if captchaErr.SessionToken != "" {
- successToken, solveErr := solveVkCaptcha(context.Background(), captchaErr, dialer)
- if solveErr != nil {
- return "", "", "", fmt.Errorf("auto captcha solve error: %w", solveErr)
+ // If solving failed (auto or manual) or timed out
+ if solveErr != nil {
+ log.Printf("[STREAM %d] [Captcha] Failed to solve (attempt %d): %v", streamID, attempt+1, solveErr)
+
+ if attempt < maxAutoAttempts-1 {
+ log.Printf("[STREAM %d] [Captcha] Backing off for 10 seconds before next auto attempt...", streamID)
+ select {
+ case <-ctx.Done():
+ return "", "", "", ctx.Err()
+ case <-time.After(10 * time.Second):
+ }
+ continue
+ } else if attempt == maxAutoAttempts-1 {
+ log.Printf("[STREAM %d] [Captcha] Backing off for 30 seconds before manual fallback...", streamID)
+ select {
+ case <-ctx.Done():
+ return "", "", "", ctx.Err()
+ case <-time.After(30 * time.Second):
+ }
+ continue
}
- if captchaErr.CaptchaAttempt == "0" || captchaErr.CaptchaAttempt == "" {
- captchaErr.CaptchaAttempt = "1"
+ // Engage global lockout to protect API
+ globalCaptchaLockout.Store(time.Now().Add(60 * time.Second).Unix())
+
+ // If we have 0 streams alive, this is fatal
+ if connectedStreams.Load() == 0 {
+ log.Printf("[STREAM %d] [FATAL] 0 connected streams and manual captcha failed/timed out.", streamID)
+ return "", "", "", fmt.Errorf("FATAL_CAPTCHA_FAILED_NO_STREAMS")
}
- 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
+ return "", "", "", fmt.Errorf("CAPTCHA_WAIT_REQUIRED")
+ }
+
+ 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 +939,45 @@ 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, 150)
- 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, 150)
- 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:")
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 +1106,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 +1139,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 +1181,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(),
@@ -766,7 +1216,7 @@ func getYandexCreds(link string) (string, string, string, error) {
},
}
- if debug {
+ if isDebug {
b, _ := json.MarshalIndent(req1, "", " ")
log.Printf("Sending HELLO:\n%s", string(b))
}
@@ -784,7 +1234,7 @@ func getYandexCreds(link string) (string, string, string, error) {
if err != nil {
return "", "", "", fmt.Errorf("ws read: %w", err)
}
- if debug {
+ if isDebug {
s := string(msg)
if len(s) > 800 {
s = s[:800] + "...(truncated)"
@@ -830,7 +1280,15 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net.
CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
ConnectionIDGenerator: dtls.OnlySendCIDGenerator(),
}
- ctx1, cancel := context.WithTimeout(ctx, 30*time.Second)
+
+ select {
+ case handshakeSem <- struct{}{}:
+ defer func() { <-handshakeSem }()
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+
+ ctx1, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
dtlsConn, err := dtls.Client(conn, peer, config)
if err != nil {
@@ -843,14 +1301,12 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net.
return dtlsConn, nil
}
-func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.PacketConn, connchan chan<- net.PacketConn, okchan chan<- struct{}, c chan<- error) {
+func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.PacketConn, inboundChan <-chan *UDPPacket, connchan chan<- net.PacketConn, okchan chan<- struct{}, streamID int) error {
time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond)
- var err error = nil
- defer func() { c <- err }()
dtlsctx, dtlscancel := context.WithCancel(ctx)
defer dtlscancel()
- var conn1, conn2 net.PacketConn
- conn1, conn2 = connutil.AsyncPacketPipe()
+
+ conn1, conn2 := connutil.AsyncPacketPipe()
go func() {
for {
select {
@@ -862,19 +1318,16 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
}()
dtlsConn, err1 := dtlsFunc(dtlsctx, conn1, peer)
if err1 != nil {
- err = fmt.Errorf("failed to connect DTLS: %s", err1)
- return
+ return fmt.Errorf("failed to connect DTLS: %s", err1)
}
defer func() {
if closeErr := dtlsConn.Close(); closeErr != nil {
- err = fmt.Errorf("failed to close DTLS connection: %s", closeErr)
- return
+ log.Printf("[STREAM %d] failed to close DTLS connection: %s", streamID, closeErr)
}
- log.Printf("Closed DTLS connection\n")
+ log.Printf("[STREAM %d] Closed DTLS connection\n", streamID)
}()
- log.Printf("Established DTLS connection!\n")
+ log.Printf("[STREAM %d] Established DTLS connection!\n", streamID)
- // Trigger the okchan safely to spawn the rest of the threads
if okchan != nil {
go func() {
select {
@@ -885,81 +1338,46 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa
}
wg := sync.WaitGroup{}
- wg.Add(2)
+ wg.Add(1)
context.AfterFunc(dtlsctx, func() {
- if err := listenConn.SetDeadline(time.Now()); err != nil {
- log.Printf("Failed to set listener deadline: %s", err)
- }
- if err := dtlsConn.SetDeadline(time.Now()); err != nil {
- log.Printf("Failed to set DTLS deadline: %s", err)
- }
+ _ = dtlsConn.SetDeadline(time.Now())
})
- // Start read-loop on listenConn
go func() {
- defer wg.Done()
defer dtlscancel()
- buf := make([]byte, 1600)
for {
select {
case <-dtlsctx.Done():
return
- default:
- }
- n, addr1, err1 := listenConn.ReadFrom(buf)
- if err1 != nil {
- log.Printf("Failed: %s", err1)
- return
- }
-
- globalClientWGAddr.Store(addr1) // store local WG peer address globally
-
- _, err1 = dtlsConn.Write(buf[:n])
- if err1 != nil {
- log.Printf("Failed: %s", err1)
- return
+ case pkt := <-inboundChan:
+ _, _ = dtlsConn.Write(pkt.Data[:pkt.N])
+ packetPool.Put(pkt)
}
}
}()
- // Start read-loop on dtlsConn
go func() {
defer wg.Done()
defer dtlscancel()
buf := make([]byte, 1600)
for {
- select {
- case <-dtlsctx.Done():
- return
- default:
- }
n, err1 := dtlsConn.Read(buf)
if err1 != nil {
- log.Printf("Failed: %s", err1)
return
}
- addr1, ok := globalClientWGAddr.Load().(net.Addr)
- if !ok {
- // Safely drop packet if wireguard hasn't sent an initial packet yet
- continue
- }
-
- _, err1 = listenConn.WriteTo(buf[:n], addr1)
- if err1 != nil {
- log.Printf("Failed: %s", err1)
- return
+ // Send back to the active WG client
+ if peerAddr := activeLocalPeer.Load(); peerAddr != nil {
+ _, _ = listenConn.WriteTo(buf[:n], peerAddr.(net.Addr))
}
}
}()
wg.Wait()
- if err := listenConn.SetDeadline(time.Time{}); err != nil {
- log.Printf("Failed to clear listener deadline: %s", err)
- }
if err := dtlsConn.SetDeadline(time.Time{}); err != nil {
- log.Printf("Failed to clear DTLS deadline: %s", err)
+ log.Printf("[STREAM %d] Failed to clear DTLS deadline: %s", streamID, err)
}
+ return nil
}
type connectedUDPConn struct {
@@ -978,16 +1396,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,15 +1424,14 @@ 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
ctx1, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if turnParams.udp {
- conn, err2 := net.DialUDP("udp", nil, turnServerUdpAddr) // nolint: noctx
+ conn, err2 := net.DialUDP("udp", nil, turnServerUdpAddr)
if err2 != nil {
err = fmt.Errorf("failed to connect to TURN server: %s", err2)
return
@@ -1027,7 +1444,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
}()
turnConn = &connectedUDPConn{conn}
} else {
- conn, err2 := d.DialContext(ctx1, "tcp", turnServerAddr) // nolint: noctx
+ conn, err2 := d.DialContext(ctx1, "tcp", turnServerAddr)
if err2 != nil {
err = fmt.Errorf("failed to connect to TURN server: %s", err2)
return
@@ -1046,8 +1463,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,95 +1482,88 @@ 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
}
+
+ // Reset error count on successful allocation
+ getStreamCache(streamID).errorCount.Store(0)
+
+ // Safely track active streams globally
+ connectedStreams.Add(1)
defer func() {
+ connectedStreams.Add(-1)
if err1 := relayConn.Close(); err1 != nil {
err = fmt.Errorf("failed to close TURN allocated connection: %s", err1)
}
}()
- // The relayConn's local address is actually the transport
- // address assigned on the TURN server.
- log.Printf("relayed-address=%s", relayConn.LocalAddr().String())
+ if isDebug {
+ log.Printf("[STREAM %d] relayed-address=%s", streamID, relayConn.LocalAddr().String())
+ }
wg := sync.WaitGroup{}
- wg.Add(2)
- turnctx, turncancel := context.WithCancel(context.Background())
+ wg.Add(1)
+ turnctx, turncancel := context.WithCancel(ctx)
context.AfterFunc(turnctx, func() {
if err := relayConn.SetDeadline(time.Now()); err != nil {
log.Printf("Failed to set relay deadline: %s", err)
}
- if err := conn2.SetDeadline(time.Now()); err != nil {
- log.Printf("Failed to set upstream deadline: %s", err)
- }
+ // Do not set conn2 deadline (conn2 can sometimes be listenConn if direct mode is used)
})
var internalPipeAddr atomic.Value
- // Start read-loop on conn2 (output of DTLS)
+
go func() {
- defer wg.Done()
defer turncancel()
buf := make([]byte, 1600)
for {
- select {
- case <-turnctx.Done():
+ if turnctx.Err() != nil {
return
- default:
}
n, addr1, err1 := conn2.ReadFrom(buf)
if err1 != nil {
- log.Printf("Failed: %s", err1)
+ return
+ }
+ if turnctx.Err() != nil {
return
}
- internalPipeAddr.Store(addr1) // store local async pipe peer
+ internalPipeAddr.Store(addr1)
_, err1 = relayConn.WriteTo(buf[:n], peer)
if err1 != nil {
- log.Printf("Failed: %s", err1)
return
}
}
}()
- // Start read-loop on relayConn
go func() {
defer wg.Done()
defer turncancel()
buf := make([]byte, 1600)
for {
- select {
- case <-turnctx.Done():
- return
- default:
- }
n, _, err1 := relayConn.ReadFrom(buf)
if err1 != nil {
- log.Printf("Failed: %s", err1)
return
}
- addr1, ok := internalPipeAddr.Load().(net.Addr)
- if !ok {
- log.Printf("Failed: no listener ip")
- return
+ addr1 := internalPipeAddr.Load()
+ if addr1 == nil {
+ continue
}
- _, err1 = conn2.WriteTo(buf[:n], addr1)
+ _, err1 = conn2.WriteTo(buf[:n], addr1.(net.Addr))
if err1 != nil {
- log.Printf("Failed: %s", err1)
return
}
}
@@ -1164,105 +1573,83 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
if err := relayConn.SetDeadline(time.Time{}); err != nil {
log.Printf("Failed to clear relay deadline: %s", err)
}
- if err := conn2.SetDeadline(time.Time{}); err != nil {
- log.Printf("Failed to clear upstream deadline: %s", err)
- }
}
-func oneDtlsConnectionLoop(ctx context.Context, peer *net.UDPAddr, listenConnChan <-chan net.PacketConn, connchan chan<- net.PacketConn, okchan chan<- struct{}) {
+func oneDtlsConnectionLoop(ctx context.Context, peer *net.UDPAddr, listenConn net.PacketConn, inboundChan <-chan *UDPPacket, connchan chan<- net.PacketConn, okchan chan<- struct{}, streamID int) {
for {
select {
case <-ctx.Done():
return
- case listenConn := <-listenConnChan:
- c := make(chan error)
- go oneDtlsConnection(ctx, peer, listenConn, connchan, okchan, c)
- if err := <-c; err != nil {
- log.Printf("%s", err)
+ default:
+ err := oneDtlsConnection(ctx, peer, listenConn, inboundChan, connchan, okchan, streamID)
+ if err != nil {
+ if time.Now().Unix() < globalCaptchaLockout.Load() && strings.Contains(err.Error(), "context deadline exceeded") {
+ continue
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(time.Duration(10+rand.Intn(20)) * time.Second):
+ }
}
}
}
}
-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)
- if err := <-c; err != nil {
- log.Printf("%s", err)
- }
- }
- }
-}
-
-type turnCred struct {
- user, pass, addr string
-}
+ go oneTurnConnection(ctx, turnParams, peer, conn2, streamID, c)
-// 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)
+ if err := <-c; err != nil {
+ if strings.Contains(err.Error(), "FATAL_CAPTCHA") {
+ log.Printf("[STREAM %d] Fatal manual captcha error. Shutting down application.", streamID)
+ if globalAppCancel != nil {
+ globalAppCancel()
+ }
+ return
+ }
+ if strings.Contains(err.Error(), "CAPTCHA_WAIT_REQUIRED") {
+ if !strings.Contains(err.Error(), "global lockout active") {
+ log.Printf("[STREAM %d] Backing off for 60 seconds to avoid IP ban...", streamID)
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(60 * time.Second):
+ }
+ } else {
+ lockoutEnd := globalCaptchaLockout.Load()
+ sleepDuration := time.Until(time.Unix(lockoutEnd, 0))
+ if sleepDuration < 0 {
+ sleepDuration = 5 * time.Second
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(sleepDuration):
+ }
+ }
+ } else {
+ log.Printf("[STREAM %d] %s", streamID, err)
+ time.Sleep(2 * time.Second)
}
-
- 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())
+ globalAppCancel = cancel
defer cancel()
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT)
@@ -1286,6 +1673,7 @@ func main() { //nolint:cyclop
n := flag.Int("n", 0, "connections to TURN (default 10 for VK, 1 for Yandex)")
udp := flag.Bool("udp", false, "connect to TURN with UDP")
direct := flag.Bool("no-dtls", false, "connect without obfuscation. DO NOT USE")
+ debugFlag := flag.Bool("debug", false, "enable debug logging")
flag.Parse()
if *peerAddr == "" {
log.Panicf("Need peer address!")
@@ -1298,6 +1686,8 @@ func main() { //nolint:cyclop
log.Panicf("Need either vk-link or yandex-link!")
}
+ isDebug = *debugFlag
+
var link string
var getCreds getCredsFunc
if *vklink != "" {
@@ -1305,13 +1695,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 +1709,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,77 +1719,95 @@ 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)
- listenConn, err := net.ListenPacket("udp", *listen) // nolint: noctx
+ listenConn, err := net.ListenPacket("udp", *listen)
if err != nil {
log.Panicf("Failed to listen: %s", err)
}
context.AfterFunc(ctx, func() {
if closeErr := listenConn.Close(); closeErr != nil {
- log.Panicf("Failed to close local connection: %s", closeErr)
+ log.Printf("Failed to close local connection: %s", closeErr)
}
})
+
+ numStreams := *n
+ if numStreams <= 0 {
+ numStreams = 1
+ }
+
+ // Shared Worker Pool Queue for Aggregation
+ inboundChan := make(chan *UDPPacket, 2000)
+
go func() {
for {
- select {
- case <-ctx.Done():
+ pkt := packetPool.Get().(*UDPPacket)
+ nRead, addr, err := listenConn.ReadFrom(pkt.Data)
+ if err != nil {
return
- case listenConnChan <- listenConn:
+ }
+
+ // Save the local WireGuard peer address
+ current := activeLocalPeer.Load()
+ if current == nil || current.(net.Addr).String() != addr.String() {
+ activeLocalPeer.Store(addr)
+ }
+
+ pkt.N = nRead
+
+ select {
+ case inboundChan <- pkt:
+ default:
+ // Drop the packet only if the global queue is completely full
+ packetPool.Put(pkt)
}
}
}()
wg1 := sync.WaitGroup{}
t := time.Tick(200 * time.Millisecond)
+
if *direct {
- for i := 0; i < *n; i++ {
- wg1.Add(1)
- go func() {
- defer wg1.Done()
- oneTurnConnectionLoop(ctx, params, peer, listenConnChan, t)
- }()
- }
- } else {
- okchan := make(chan struct{})
- connchan := make(chan net.PacketConn)
+ log.Panicf("Direct mode not supported with dispatcher")
+ }
+ okchan := make(chan struct{})
+ connchan := make(chan net.PacketConn)
+ wg1.Add(1)
+ go func() {
+ defer wg1.Done()
+ oneDtlsConnectionLoop(ctx, peer, listenConn, inboundChan, connchan, okchan, 1)
+ }()
+ wg1.Add(1)
+ go func() {
+ defer wg1.Done()
+ oneTurnConnectionLoop(ctx, params, peer, connchan, t, 1)
+ }()
+
+ select {
+ case <-okchan:
+ case <-ctx.Done():
+ }
+
+ for i := 1; i < numStreams; i++ {
+ cchan := make(chan net.PacketConn)
wg1.Add(1)
- go func() {
+ go func(streamID int) {
defer wg1.Done()
- oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, okchan)
- }()
-
+ oneDtlsConnectionLoop(ctx, peer, listenConn, inboundChan, cchan, nil, streamID)
+ }(i)
wg1.Add(1)
- go func() {
+ go func(streamID int) {
defer wg1.Done()
- oneTurnConnectionLoop(ctx, params, peer, connchan, t)
- }()
-
- select {
- case <-okchan:
- case <-ctx.Done():
- }
- for i := 0; i < *n-1; i++ {
- connchan := make(chan net.PacketConn)
- wg1.Add(1)
- go func() {
- defer wg1.Done()
- oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, nil)
- }()
- wg1.Add(1)
- go func() {
- defer wg1.Done()
- oneTurnConnectionLoop(ctx, params, peer, connchan, t)
- }()
- }
+ oneTurnConnectionLoop(ctx, params, peer, cchan, t, streamID)
+ }(i)
}
wg1.Wait()
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, `
+