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, ""): + 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, ` + + + + +

Solve the Captcha

+captcha +
+
+
+
`, captchaImg) + }) + + mux.HandleFunc("/solve", func(w http.ResponseWriter, r *http.Request) { + notifyKey(keyCh, r.URL.Query().Get("key")) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = fmt.Fprint(w, `

Done!

`) + }) + + return runCaptchaServerAndWait(mux, localCaptchaOrigin(), keyCh, "captcha HTTP server error") +} + +func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string, error) { + keyCh := make(chan string, 1) + + targetURL, err := neturl.Parse(redirectURI) + if err != nil { + return "", fmt.Errorf("invalid redirect URI: %v", err) + } + + transport := newCaptchaProxyTransport(dialer) + + proxy := &httputil.ReverseProxy{ + Transport: transport, + Rewrite: func(req *httputil.ProxyRequest) { + rewriteProxyRequest(req.Out, targetURL) + }, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + log.Printf("captcha proxy error for %s: %v", r.URL.String(), err) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusBadGateway) + _, _ = fmt.Fprintf(w, `

Captcha proxy error

%v

`, err) + }, + ModifyResponse: func(res *http.Response) error { + rewriteProxyCookies(res.Header) + + if res.StatusCode >= 300 && res.StatusCode < 400 { + if loc := res.Header.Get("Location"); loc != "" { + if strings.HasPrefix(loc, "/") { + res.Header.Set("Location", loc) + } else if strings.HasPrefix(loc, targetOrigin(targetURL)) { + res.Header.Set("Location", strings.Replace(loc, targetOrigin(targetURL), localCaptchaOrigin(), 1)) + } + } + } + + contentType := res.Header.Get("Content-Type") + shouldInspectBody := strings.Contains(contentType, "text/html") || strings.Contains(res.Request.URL.Path, "captchaNotRobot.check") + if !shouldInspectBody { + return nil + } + + reader := res.Body + if res.Header.Get("Content-Encoding") == "gzip" { + gzReader, err := gzip.NewReader(res.Body) + if err == nil { + reader = gzReader + defer func() { + if err := gzReader.Close(); err != nil { + log.Printf("failed to close gzip reader: %v", err) + } + }() + } + } + + bodyBytes, err := io.ReadAll(reader) + if err != nil { + return err + } + if err := res.Body.Close(); err != nil { + return err + } + + if strings.Contains(res.Request.URL.Path, "captchaNotRobot.check") { + notifyKey(keyCh, extractSuccessToken(bodyBytes)) + } + + if strings.Contains(contentType, "text/html") { + for _, headerName := range []string{ + "Content-Security-Policy", + "Content-Security-Policy-Report-Only", + "X-Content-Security-Policy", + "X-WebKit-CSP", + "Cross-Origin-Opener-Policy", + "Cross-Origin-Embedder-Policy", + "Cross-Origin-Resource-Policy", + "X-Frame-Options", + } { + res.Header.Del(headerName) + } + + bodyBytes = []byte(rewriteCaptchaHTML(string(bodyBytes), targetURL)) + res.Header.Del("Content-Encoding") + } + + res.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + res.ContentLength = int64(len(bodyBytes)) + res.Header.Set("Content-Length", fmt.Sprint(len(bodyBytes))) + + return nil + }, + } + + mux := http.NewServeMux() + mux.HandleFunc("/local-captcha-result", func(w http.ResponseWriter, r *http.Request) { + notifyKey(keyCh, r.FormValue("token")) // r.FormValue automatically parses the form + w.Header().Set("Access-Control-Allow-Origin", "*") + _, _ = fmt.Fprint(w, "ok") + }) + + mux.HandleFunc("/generic_proxy", func(w http.ResponseWriter, r *http.Request) { + targetAuthUrl := r.URL.Query().Get("proxy_url") + targetParsed, err := neturl.Parse(targetAuthUrl) + if err != nil || targetParsed.Host == "" { + http.Error(w, "Bad URL", http.StatusBadRequest) + return + } + genericReverse := &httputil.ReverseProxy{ + Transport: transport, + Rewrite: func(req *httputil.ProxyRequest) { + req.Out.URL.Path = targetParsed.Path + req.Out.URL.RawQuery = targetParsed.RawQuery + rewriteProxyRequest(req.Out, targetParsed) + }, + } + genericReverse.ServeHTTP(w, r) + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" && targetURL.Path != "" && targetURL.Path != "/" && r.URL.RawQuery == "" { + http.Redirect(w, r, localCaptchaURLForTarget(targetURL), http.StatusTemporaryRedirect) + return + } + proxy.ServeHTTP(w, r) + }) + + return runCaptchaServerAndWait(mux, localCaptchaURLForTarget(targetURL), keyCh, "proxy HTTP server error") +} + +func openBrowser(url string) { + for _, cmd := range browserOpenCommands(runtime.GOOS, url) { + if err := exec.Command(cmd.name, cmd.args...).Start(); err == nil { + return + } + } +} + +func browserOpenCommands(goos string, url string) []browserCommand { + switch goos { + case "windows": + return []browserCommand{{name: "cmd", args: []string{"/c", "start", url}}} + case "darwin": + return []browserCommand{{name: "open", args: []string{url}}} + case "linux": + return []browserCommand{ + {name: "xdg-open", args: []string{url}}, + {name: "gio", args: []string{"open", url}}, + } + case "android": + return []browserCommand{ + {name: "termux-open-url", args: []string{url}}, + {name: "/system/bin/am", args: []string{"start", "-a", "android.intent.action.VIEW", "-d", url}}, + {name: "am", args: []string{"start", "-a", "android.intent.action.VIEW", "-d", url}}, + {name: "xdg-open", args: []string{url}}, + } + case "ios": + return []browserCommand{ + {name: "open", args: []string{url}}, + {name: "uiopen", args: []string{url}}, + } + } + return nil +} diff --git a/client/namegen.go b/client/namegen.go index e9e1804..0593c9d 100644 --- a/client/namegen.go +++ b/client/namegen.go @@ -3,40 +3,185 @@ package main import ( "fmt" "math/rand" + "strings" ) -// firstNames contains Russian first names. Add or remove names as needed. -var firstNames = []string{ - "Александр", "Дмитрий", "Максим", "Сергей", "Андрей", "Алексей", "Артём", "Илья", - "Кирилл", "Михаил", "Никита", "Матвей", "Роман", "Егор", "Арсений", "Иван", - "Денис", "Даниил", "Тимофей", "Владислав", "Игорь", "Павел", "Руслан", "Марк", - "Анна", "Мария", "Елена", "Дарья", "Анастасия", "Екатерина", "Виктория", "Ольга", - "Наталья", "Юлия", "Татьяна", "Светлана", "Ирина", "Ксения", "Алина", "Елизавета", +var maleFirstNames = []string{ + "Александр", + "Алексей", + "Андрей", + "Антон", + "Арсений", + "Артур", + "Артём", + "Богдан", + "Валерий", + "Василий", + "Виктор", + "Владислав", + "Глеб", + "Григорий", + "Даниил", + "Денис", + "Дмитрий", + "Евгений", + "Егор", + "Иван", + "Игорь", + "Илья", + "Кирилл", + "Леонид", + "Максим", + "Марк", + "Матвей", + "Михаил", + "Никита", + "Николай", + "Олег", + "Павел", + "Пётр", + "Роман", + "Руслан", + "Сергей", + "Станислав", + "Тимофей", + "Фёдор", +} + +var femaleFirstNames = []string{ + "Алина", + "Алёна", + "Анастасия", + "Ангелина", + "Анна", + "Вера", + "Вероника", + "Виктория", + "Дарья", + "Ева", + "Екатерина", + "Елена", + "Елизавета", + "Ирина", + "Кира", + "Кристина", + "Ксения", + "Любовь", + "Маргарита", + "Марина", + "Мария", + "Милана", + "Надежда", + "Наталья", + "Ольга", + "Полина", + "Светлана", + "София", + "Татьяна", + "Юлия", + "Яна", } -// lastNames contains Russian last names. Add or remove names as needed. var lastNames = []string{ - "Иванов", "Смирнов", "Кузнецов", "Попов", "Васильев", "Петров", "Соколов", "Михайлов", - "Новиков", "Федоров", "Морозов", "Волков", "Алексеев", "Лебедев", "Семенов", "Егоров", - "Павлов", "Козлов", "Степанов", "Николаев", "Орлов", "Андреев", "Макаров", "Никитин", - "Захаров", "Зайцев", "Соловьев", "Борисов", "Яковлев", "Григорьев", "Романов", "Воробьев", + "Алексеев", + "Андреев", + "Антонов", + "Баранов", + "Белов", + "Белый", + "Бельский", + "Беляев", + "Борисов", + "Васильев", + "Великий", + "Волков", + "Воробьёв", + "Григорьев", + "Давыдов", + "Егоров", + "Жуков", + "Зайцев", + "Захаров", + "Иванов", + "Калинин", + "Ковалёв", + "Козлов", + "Комаров", + "Крамской", + "Кузнецов", + "Кузьмин", + "Лебедев", + "Макаров", + "Медведев", + "Михайлов", + "Морозов", + "Никитин", + "Николаев", + "Новиков", + "Орлов", + "Островский", + "Павлов", + "Петров", + "Покровский", + "Попов", + "Раевский", + "Романов", + "Семёнов", + "Сергеев", + "Смирнов", + "Соколов", + "Соловьёв", + "Степанов", + "Тарасов", + "Титов", + "Толстой", + "Трубецкой", + "Филиппов", + "Фролов", + "Фёдоров", + "Чайковский", + "Черный", + "Яковлев", +} + +// convertToFemaleSurname handles Russian suffix rules +func convertToFemaleSurname(surname string) string { + // Handle adjective-style surnames: + if strings.HasSuffix(surname, "ий") || strings.HasSuffix(surname, "ый") || strings.HasSuffix(surname, "ой") { + return surname[:len(surname)-4] + "ая" + } + + // Handle standard possessive surnames: + if strings.HasSuffix(surname, "ов") || strings.HasSuffix(surname, "ев") || + strings.HasSuffix(surname, "ин") || strings.HasSuffix(surname, "ын") || + strings.HasSuffix(surname, "ёв") { + return surname + "а" + } + + // Foreign or unchangeable + return surname } -// generateName generates a random Russian name. -// 30% chance to generate only first name, 70% chance first + last name. -// For female names (ending in 'а' or 'я'), adds 'а' to the last name. func generateName() string { + // Decide gender first + isFemale := rand.Intn(2) == 0 + + var fn string + if isFemale { + fn = femaleFirstNames[rand.Intn(len(femaleFirstNames))] + } else { + fn = maleFirstNames[rand.Intn(len(maleFirstNames))] + } + + // 70% chance to have a last name if rand.Float32() < 0.3 { - return firstNames[rand.Intn(len(firstNames))] + return fn } - fn := firstNames[rand.Intn(len(firstNames))] ln := lastNames[rand.Intn(len(lastNames))] - - // add 'a' to the last name for females - lastChar := fn[len(fn)-2:] // 2 bytes for cyrillic - if lastChar == "а" || lastChar == "я" { - return fmt.Sprintf("%s %sа", fn, ln) + if isFemale { + ln = convertToFemaleSurname(ln) } + return fmt.Sprintf("%s %s", fn, ln) } diff --git a/client/profiles.go b/client/profiles.go index 7df1dc7..01d4f0c 100644 --- a/client/profiles.go +++ b/client/profiles.go @@ -5,51 +5,78 @@ import ( ) type Profile struct { - UserAgent string + UserAgent string + SecChUa string + SecChUaMobile string + SecChUaPlatform string } -// profiles contains realistic user-agent strings for different browsers and platforms. -// Add or remove profiles as needed. -var profiles = []Profile{ - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, +// profiles contain paired User-Agent and Client Hints strings to harden bot detection. +var profile = []Profile{ + // Windows Chrome + { + 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: `"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"Windows"`, + }, + { + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", + SecChUa: `"Chromium";v="145", "Not-A.Brand";v="99", "Google Chrome";v="145"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"Windows"`, + }, + { + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36", + SecChUa: `"Chromium";v="144", "Not-A.Brand";v="8", "Google Chrome";v="144"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"Windows"`, + }, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"}, - {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0"}, - {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"}, + // Windows Edge + { + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0", + SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"Windows"`, + }, + { + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0", + SecChUa: `"Chromium";v="145", "Not-A.Brand";v="99", "Microsoft Edge";v="145"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"Windows"`, + }, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 YaBrowser/24.1.0.0 Yowser/2.5 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 YaBrowser/24.1.2.0 Yowser/2.5 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 YaBrowser/23.12.0.0 Yowser/2.5 Safari/537.36"}, + // macOS Chrome + { + UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", + SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"macOS"`, + }, + { + UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", + SecChUa: `"Chromium";v="145", "Not-A.Brand";v="99", "Google Chrome";v="145"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"macOS"`, + }, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 OPR/112.0.0.0"}, - {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 OPR/111.0.0.0"}, - - {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"}, - - {"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"}, - - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"}, - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"}, + // Linux Chrome + { + UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", + SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"Linux"`, + }, + { + UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36", + SecChUa: `"Chromium";v="144", "Not-A.Brand";v="8", "Google Chrome";v="144"`, + SecChUaMobile: "?0", + SecChUaPlatform: `"Linux"`, + }, } -// getRandomProfile returns a random user-agent profile. +// getRandomProfile returns a paired User-Agent and Client Hints profile. func getRandomProfile() Profile { - return profiles[rand.Intn(len(profiles))] + return profile[rand.Intn(len(profile))] } diff --git a/go.mod b/go.mod index 63d5b25..34d031c 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,38 @@ module github.com/cacggghp/vk-turn-proxy go 1.25.5 require ( + github.com/bogdanfinn/fhttp v0.6.8 + github.com/bogdanfinn/tls-client v1.14.0 github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45 github.com/cbeuw/connutil v1.0.1 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/pion/dtls/v3 v3.0.11 github.com/pion/logging v0.2.4 + github.com/pion/transport/v4 v4.0.1 github.com/pion/turn/v5 v5.0.2 ) require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/bdandy/go-errors v1.2.2 // indirect + github.com/bdandy/go-socks4 v1.2.3 // indirect + github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect + github.com/bogdanfinn/utls v1.7.7-barnius // indirect + github.com/bogdanfinn/websocket v1.5.5-barnius // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/miekg/dns v1.1.69 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/stun/v3 v3.1.1 // indirect - github.com/pion/transport/v4 v4.0.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect - golang.org/x/sync v0.18.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index bbaf68e..3303358 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,19 @@ +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= +github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM= +github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic= +github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI= +github.com/bogdanfinn/fhttp v0.6.8 h1:LiQyHOY3i0QoxxNB7nq27/nGNNbtPj0fuBPozhR7Ws4= +github.com/bogdanfinn/fhttp v0.6.8/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M= +github.com/bogdanfinn/quic-go-utls v1.0.9-utls h1:tV6eDEiRbRCcepALSzxR94JUVD3N3ACIiRLgyc2Ep8s= +github.com/bogdanfinn/quic-go-utls v1.0.9-utls/go.mod h1:aHph9B9H9yPOt5xnhWKSOum27DJAqpiHzwX+gjvaXcg= +github.com/bogdanfinn/tls-client v1.14.0 h1:vyk7Cn4BIvLAGVuMfb0tP22OqogfO1lYamquQNEZU1A= +github.com/bogdanfinn/tls-client v1.14.0/go.mod h1:LsU6mXVn8MOFDwTkyRfI7V1BZM1p0wf2ZfZsICW/1fM= +github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU= +github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg= +github.com/bogdanfinn/websocket v1.5.5-barnius h1:bY+qnxpai1qe7Jmjx+Sds/cmOSpuuLoR8x61rWltjOI= +github.com/bogdanfinn/websocket v1.5.5-barnius/go.mod h1:gvvEw6pTKHb7yOiFvIfAFTStQWyrm25BMVCTj5wRSsI= github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45 h1:0b2i5TvZm8FVcuHP1288k+DEu1XM26DtRjcidOxpGXs= github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45/go.mod h1:NU7MdmhQD8Ounc0760w90fL6nxI2lxjlnIaN6qWzNIU= github.com/cbeuw/connutil v1.0.1 h1:LWuNYjwm7JEDYG/ISAO1TfU4G+q2dA5NhR97eq2roCA= @@ -12,6 +28,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/pion/dtls/v3 v3.0.11 h1:zqn8YhoAU7d9whsWLhNiQlbB8QdpJj8XQVSc5ImUons= @@ -28,26 +46,40 @@ github.com/pion/turn/v5 v5.0.2 h1:GHlDk+fiegz+yibb3ch+tK+iPFokoVWiM+aVJakySqA= github.com/pion/turn/v5 v5.0.2/go.mod h1:cumcsSEF2ytAtDhDwkYgYhv1uJ3AOP7a4pFt0NL/snY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= +github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= diff --git a/server/main.go b/server/main.go index 819f9b5..effce98 100644 --- a/server/main.go +++ b/server/main.go @@ -17,6 +17,8 @@ import ( "github.com/pion/dtls/v3/pkg/crypto/selfsign" ) +const idleTimeout = 2 * time.Minute + func main() { listen := flag.String("listen", "0.0.0.0:56000", "listen on ip:port") connect := flag.String("connect", "", "connect to ip:port") @@ -150,7 +152,7 @@ func main() { return default: } - if err1 := conn.SetReadDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + if err1 := conn.SetReadDeadline(time.Now().Add(idleTimeout)); err1 != nil { log.Printf("Failed: %s", err1) return } @@ -160,7 +162,7 @@ func main() { return } - if err1 := serverConn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + if err1 := serverConn.SetWriteDeadline(time.Now().Add(idleTimeout)); err1 != nil { log.Printf("Failed: %s", err1) return } @@ -181,7 +183,7 @@ func main() { return default: } - if err1 := serverConn.SetReadDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + if err1 := serverConn.SetReadDeadline(time.Now().Add(idleTimeout)); err1 != nil { log.Printf("Failed: %s", err1) return } @@ -191,7 +193,7 @@ func main() { return } - if err1 := conn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + if err1 := conn.SetWriteDeadline(time.Now().Add(idleTimeout)); err1 != nil { log.Printf("Failed: %s", err1) return }