diff --git a/README.md b/README.md index 4487677..97cfced 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,43 @@ -# vk-turn-proxy -Проброс трафика WireGuard через TURN сервера VK звонков. Пакеты шифруются DTLS 1.2, затем параллельными потоками через TCP или UDP отправляются на TURN сервер по протоколу STUN ChannelData. Оттуда по UDP отправляются на ваш сервер, где расшифровываются и передаются в WireGuard. Логин/пароль от TURN генерируются из ссылки на звонок. +# Good TURN +Проброс трафика WireGuard/Hysteria через TURN сервера VK звонков или Яндекс телемоста. Пакеты шифруются DTLS 1.2, затем параллельными потоками через TCP или UDP отправляются на TURN сервер по протоколу STUN ChannelData. Оттуда по UDP отправляются на ваш сервер, где расшифровываются и передаются в WireGuard. Логин/пароль от TURN генерируются из ссылки на звонок. Только для учебных целей! ## Настройка Нам понадобится: 1. Ссылка на действующий ВК звонок: создаём свой (нужен аккаунт вк), или гуглим `"https://vk.com/call/join/"`. Ссылка действительна вечно, если не нажимать "завершить звонок для всех" -2. VPS с установленным WireGuard -3. Для андроида: скачать Termux из F-Droid +2. Или ссыска на звонок Яндекс телемоста: `"https://telemost.yandex.ru/j/"`. Её лучше не гуглить, так как видно подключение к конференции +3. VPS с установленным WireGuard +4. Для андроида: скачать Termux из F-Droid ### Сервер -`./server -listen 0.0.0.0:56000 -connect 127.0.0.1:<порт wg>` +``` +./server -listen 0.0.0.0:56000 -connect 127.0.0.1:<порт wg> +``` ### Клиент #### Android -1. В клиентском конфиге WireGuard меняем адрес сервера на `127.0.0.1:9000`, ставим MTU 1280 -2. **Добавляем Termux в исключения WireGuard. Нажимаем "сохранить".** - +- В клиентском конфиге WireGuard меняем адрес сервера на `127.0.0.1:9000`, ставим MTU 1280 +- **Добавляем Termux в исключения WireGuard. Нажимаем "сохранить".** В Termux: - -3. `termux-wake-lock` Телефон не будет уходить в глубокий сон, так что на ночь ставьте на зарядку. Чтобы отключить: `termux-wake-unlock` -4. Копируем бинарник в локальную папку, даём права на исполнение: -5. `cp /sdcard/Download/client-android ./` -6. `chmod 777 ./client-android` -7. `./client-android -peer :56000 -link -listen 127.0.0.1:9000` +``` +termux-wake-lock +``` +Телефон не будет уходить в глубокий сон, так что на ночь ставьте на зарядку. Чтобы отключить: +``` +termux-wake-unlock +``` +Копируем бинарник в локальную папку, даём права на исполнение: +``` +cp /sdcard/Download/client-android ./ +chmod 777 ./client-android +``` +Запускаем: +``` +./client-android -peer :56000 -vk-link -listen 127.0.0.1:9000 +``` +Или +``` +./client-android -udp -turn 5.255.211.241 -peer :56000 -yandex-link -listen 127.0.0.1:9000 +``` **Если после включения VPN в терминале вылезают ошибки DNS, попробуйте в Wireguard включить VPN только для нужных приложений.** #### Linux @@ -29,7 +45,13 @@ Скрипт будет добавлять маршруты к нужным ip: -`./client-linux -peer :56000 -link -listen 127.0.0.1:9000 | sudo routes.sh` +``` +./client-linux -peer :56000 -vk-link -listen 127.0.0.1:9000 | sudo routes.sh +``` + +``` +./client-linux -udp -turn 5.255.211.241 -peer :56000 -yandex-link -listen 127.0.0.1:9000 | sudo routes.sh +``` Не включайте впн, пока программа не установит соединение! В отличие от андроида, здесь часть запросов будет идти через впн (dns и запрос подключения к turn) #### Windows @@ -37,15 +59,43 @@ В PowerShell от Администратора (чтобы скрипт прописывал маршруты): -`./client.exe -peer :56000 -link -listen 127.0.0.1:9000 | routes.ps1` +``` +./client.exe -peer :56000 -vk-link -listen 127.0.0.1:9000 | routes.ps1 +``` + +``` +./client.exe -udp -turn 5.255.211.241 -peer :56000 -yandex-link -listen 127.0.0.1:9000 | routes.ps1 +``` Не включайте впн, пока программа не установит соединение! В отличие от андроида, здесь часть запросов будет идти через впн (dns и запрос подключения к turn) ### Если не работает -С помощью опции `-turn` можно указать адрес TURN сервера вручную. Это должен быть сервер ВК, Макса или Одноклассников. Возможно потом составлю список. +С помощью опции `-turn` можно указать адрес TURN сервера вручную. Это должен быть сервер ВК, Макса или Одноклассников (ссылка вк) или Яндекса (ссылка яндекса). Возможно потом составлю список. Если не работает TCP, попробуйте добавить флаг `-udp`. -Добавьте флаг `-n 1` для более стабильного подключения в 1 поток (ограничение 5 Мбит/с) +Добавьте флаг `-n 1` для более стабильного подключения в 1 поток (ограничение 5 Мбит/с для ВК) + +## Яндекс телемост +В отличие от ВК, сервера яндекса не ограничивают скорость, так что по умолчанию стоит `-n 1`. Увеличение этого числа может привести к временной блокировке по IP из-за переполнения конференции фейковыми участниками. + +В режиме `-udp` скорость обычно больше + +Большинство диапазонов IP TURN серверов Яндекса не работают, указывайте вручную через `-turn` +
+ + Рабочие IP + + + + 5.255.211.241 + 5.255.211.242 + 5.255.211.243 + 5.255.211.245 + 5.255.211.246 + + +
+Спасибо https://github.com/KillTheCensorship/Turnel за часть кода :) ## v2ray @@ -153,4 +203,7 @@ } ``` - \ No newline at end of file + + +## Direct mode +С флагом `-no-dtls` можно отправлять пакеты без обфускации DTLS и подключаться к обычным серверам Wireguard. Может привести к бану от вк/яндекса. diff --git a/client/main.go b/client/main.go index 2884efa..368040c 100644 --- a/client/main.go +++ b/client/main.go @@ -16,6 +16,7 @@ import ( "net/http" "os" "os/signal" + "strings" "sync" "sync/atomic" "syscall" @@ -23,49 +24,53 @@ import ( "github.com/cbeuw/connutil" "github.com/google/uuid" + "github.com/gorilla/websocket" "github.com/pion/dtls/v3" "github.com/pion/dtls/v3/pkg/crypto/selfsign" "github.com/pion/logging" - "github.com/pion/turn/v4" + "github.com/pion/turn/v5" ) -func doRequest(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, - }, - } - req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data))) - if err != nil { - return nil, err - } +type getCredsFunc func(string) (string, string, string, error) + +func getVkCreds(link string) (string, string, string, error) { + 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, + }, + } + defer client.CloseIdleConnections() + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data))) + if err != nil { + return nil, err + } - req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0") - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0") + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - httpResp, err := client.Do(req) - if err != nil { - return nil, err - } - defer httpResp.Body.Close() + httpResp, err := client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() - body, err := io.ReadAll(httpResp.Body) - if err != nil { - return nil, err - } + body, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, err + } - err = json.Unmarshal(body, &resp) - if err != nil { - return nil, err - } + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, err + } - return resp, nil -} + return resp, nil + } -func getCreds(link string) (string, string, string, error) { var resp map[string]interface{} defer func() { if r := recover(); r != nil { @@ -135,7 +140,292 @@ func getCreds(link string) (string, string, string, error) { pass := resp["turn_server"].(map[string]interface{})["credential"].(string) turn := resp["turn_server"].(map[string]interface{})["urls"].([]interface{})[0].(string) - return user, pass, turn, nil + clean := strings.Split(turn, "?")[0] + address := strings.TrimPrefix(strings.TrimPrefix(clean, "turn:"), "turns:") + + return user, pass, address, nil +} + +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") + const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0" + + type ConferenceResponse struct { + URI string `json:"uri"` + RoomID string `json:"room_id"` + PeerID string `json:"peer_id"` + ClientConfiguration struct { + MediaServerURL string `json:"media_server_url"` + } `json:"client_configuration"` + Credentials string `json:"credentials"` + } + + type PartMeta struct { + Name string `json:"name"` + Role string `json:"role"` + Description string `json:"description"` + SendAudio bool `json:"sendAudio"` + SendVideo bool `json:"sendVideo"` + } + + type PartAttrs struct { + Name string `json:"name"` + Role string `json:"role"` + Description string `json:"description"` + } + + type SdkInfo struct { + Implementation string `json:"implementation"` + Version string `json:"version"` + UserAgent string `json:"userAgent"` + HwConcurrency int `json:"hwConcurrency"` + } + + type Capabilities struct { + OfferAnswerMode []string `json:"offerAnswerMode"` + InitialSubscriberOffer []string `json:"initialSubscriberOffer"` + SlotsMode []string `json:"slotsMode"` + SimulcastMode []string `json:"simulcastMode"` + SelfVadStatus []string `json:"selfVadStatus"` + DataChannelSharing []string `json:"dataChannelSharing"` + VideoEncoderConfig []string `json:"videoEncoderConfig"` + DataChannelVideoCodec []string `json:"dataChannelVideoCodec"` + BandwidthLimitationReason []string `json:"bandwidthLimitationReason"` + SdkDefaultDeviceManagement []string `json:"sdkDefaultDeviceManagement"` + JoinOrderLayout []string `json:"joinOrderLayout"` + PinLayout []string `json:"pinLayout"` + SendSelfViewVideoSlot []string `json:"sendSelfViewVideoSlot"` + ServerLayoutTransition []string `json:"serverLayoutTransition"` + SdkPublisherOptimizeBitrate []string `json:"sdkPublisherOptimizeBitrate"` + SdkNetworkLostDetection []string `json:"sdkNetworkLostDetection"` + SdkNetworkPathMonitor []string `json:"sdkNetworkPathMonitor"` + PublisherVp9 []string `json:"publisherVp9"` + SvcMode []string `json:"svcMode"` + SubscriberOfferAsyncAck []string `json:"subscriberOfferAsyncAck"` + SvcModes []string `json:"svcModes"` + ReportTelemetryModes []string `json:"reportTelemetryModes"` + KeepDefaultDevicesModes []string `json:"keepDefaultDevicesModes"` + } + + type HelloPayload struct { + ParticipantMeta PartMeta `json:"participantMeta"` + ParticipantAttributes PartAttrs `json:"participantAttributes"` + SendAudio bool `json:"sendAudio"` + SendVideo bool `json:"sendVideo"` + SendSharing bool `json:"sendSharing"` + ParticipantID string `json:"participantId"` + RoomID string `json:"roomId"` + ServiceName string `json:"serviceName"` + Credentials string `json:"credentials"` + CapabilitiesOffer Capabilities `json:"capabilitiesOffer"` + SdkInfo SdkInfo `json:"sdkInfo"` + SdkInitializationID string `json:"sdkInitializationId"` + DisablePublisher bool `json:"disablePublisher"` + DisableSubscriber bool `json:"disableSubscriber"` + DisableSubscriberAudio bool `json:"disableSubscriberAudio"` + } + + type HelloRequest struct { + UID string `json:"uid"` + Hello HelloPayload `json:"hello"` + } + + type FlexUrls []string + + type WSSResponse struct { + UID string `json:"uid"` + ServerHello struct { + RtcConfiguration struct { + IceServers []struct { + Urls FlexUrls `json:"urls"` + Username string `json:"username,omitempty"` + Credential string `json:"credential,omitempty"` + } `json:"iceServers"` + } `json:"rtcConfiguration"` + } `json:"serverHello"` + } + + type WSSAck struct { + Uid string `json:"uid"` + Ack struct { + Status struct { + Code string `json:"code"` + } `json:"status"` + } `json:"ack"` + } + + type WSSData struct { + ParticipantId string + RoomId string + Credentials string + Wss string + } + + endpoint := "https://" + telemostConfHost + telemostConfPath + client := &http.Client{ + Timeout: 20 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + }, + } + defer client.CloseIdleConnections() + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return "", "", "", err + } + req.Header.Set("User-Agent", userAgent) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Referer", "https://telemost.yandex.ru/") + req.Header.Set("Origin", "https://telemost.yandex.ru") + req.Header.Set("Client-Instance-Id", uuid.New().String()) + + resp, err := client.Do(req) + if err != nil { + return "", "", "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", "", "", fmt.Errorf("GetConference: status=%s body=%s", resp.Status, string(body)) + } + + var result ConferenceResponse + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", "", "", fmt.Errorf("decode conf: %v", err) + } + data := WSSData{ + ParticipantId: result.PeerID, + RoomId: result.RoomID, + Credentials: result.Credentials, + Wss: result.ClientConfiguration.MediaServerURL, + } + h := http.Header{} + h.Set("Origin", "https://telemost.yandex.ru") + h.Set("User-Agent", userAgent) + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + dialer := websocket.Dialer{} + conn, _, err := dialer.DialContext(ctx, data.Wss, h) + if err != nil { + return "", "", "", fmt.Errorf("ws dial: %w", err) + } + defer conn.Close() + + req1 := HelloRequest{ + UID: uuid.New().String(), + Hello: HelloPayload{ + ParticipantMeta: PartMeta{ + Name: "Гость", + Role: "SPEAKER", + Description: "", + SendAudio: false, + SendVideo: false, + }, + ParticipantAttributes: PartAttrs{ + Name: "Гость", + Role: "SPEAKER", + Description: "", + }, + SendAudio: false, + SendVideo: false, + SendSharing: false, + + ParticipantID: data.ParticipantId, + RoomID: data.RoomId, + ServiceName: "telemost", + Credentials: data.Credentials, + SdkInfo: SdkInfo{ + Implementation: "browser", + Version: "5.15.0", + UserAgent: userAgent, + HwConcurrency: 4, + }, + SdkInitializationID: uuid.New().String(), + DisablePublisher: false, + DisableSubscriber: false, + DisableSubscriberAudio: false, + CapabilitiesOffer: Capabilities{ + OfferAnswerMode: []string{"SEPARATE"}, + InitialSubscriberOffer: []string{"ON_HELLO"}, + SlotsMode: []string{"FROM_CONTROLLER"}, + SimulcastMode: []string{"DISABLED"}, + SelfVadStatus: []string{"FROM_SERVER"}, + DataChannelSharing: []string{"TO_RTP"}, + VideoEncoderConfig: []string{"NO_CONFIG"}, + DataChannelVideoCodec: []string{"VP8"}, + BandwidthLimitationReason: []string{"BANDWIDTH_REASON_DISABLED"}, + SdkDefaultDeviceManagement: []string{"SDK_DEFAULT_DEVICE_MANAGEMENT_DISABLED"}, + JoinOrderLayout: []string{"JOIN_ORDER_LAYOUT_DISABLED"}, + PinLayout: []string{"PIN_LAYOUT_DISABLED"}, + SendSelfViewVideoSlot: []string{"SEND_SELF_VIEW_VIDEO_SLOT_DISABLED"}, + ServerLayoutTransition: []string{"SERVER_LAYOUT_TRANSITION_DISABLED"}, + SdkPublisherOptimizeBitrate: []string{"SDK_PUBLISHER_OPTIMIZE_BITRATE_DISABLED"}, + SdkNetworkLostDetection: []string{"SDK_NETWORK_LOST_DETECTION_DISABLED"}, + SdkNetworkPathMonitor: []string{"SDK_NETWORK_PATH_MONITOR_DISABLED"}, + PublisherVp9: []string{"PUBLISH_VP9_DISABLED"}, + SvcMode: []string{"SVC_MODE_DISABLED"}, + SubscriberOfferAsyncAck: []string{"SUBSCRIBER_OFFER_ASYNC_ACK_DISABLED"}, + SvcModes: []string{"FALSE"}, + ReportTelemetryModes: []string{"TRUE"}, + KeepDefaultDevicesModes: []string{"TRUE"}, + }, + }, + } + + if debug { + b, _ := json.MarshalIndent(req1, "", " ") + log.Printf("Sending HELLO:\n%s", string(b)) + } + + if err := conn.WriteJSON(req1); err != nil { + return "", "", "", fmt.Errorf("ws write: %w", err) + } + + conn.SetReadDeadline(time.Now().Add(15 * time.Second)) + + for { + _, msg, err := conn.ReadMessage() + if err != nil { + return "", "", "", fmt.Errorf("ws read: %w", err) + } + if debug { + s := string(msg) + if len(s) > 800 { + s = s[:800] + "...(truncated)" + } + log.Printf("WSS recv: %s", s) + } + + var ack WSSAck + if err := json.Unmarshal(msg, &ack); err == nil && ack.Ack.Status.Code != "" { + continue + } + + var resp WSSResponse + if err := json.Unmarshal(msg, &resp); err == nil { + ice := resp.ServerHello.RtcConfiguration.IceServers + for _, s := range ice { + for _, u := range s.Urls { + if !strings.HasPrefix(u, "turn:") && !strings.HasPrefix(u, "turns:") { + continue + } + if strings.Contains(u, "transport=tcp") { + continue + } + clean := strings.Split(u, "?")[0] + address := strings.TrimPrefix(strings.TrimPrefix(clean, "turn:"), "turns:") + + return s.Username, s.Credential, address, nil + } + } + } + } } func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net.Conn, error) { @@ -163,9 +453,9 @@ 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{}, c1 chan<- error) { +func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.PacketConn, connchan chan<- net.PacketConn, okchan chan<- struct{}, c chan<- error) { var err error = nil - defer func() { c1 <- err }() + defer func() { c <- err }() dtlsctx, dtlscancel := context.WithCancel(ctx) defer dtlscancel() var conn1, conn2 net.PacketConn @@ -271,20 +561,43 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa dtlsConn.SetDeadline(time.Time{}) } -func oneTurnConnection(ctx context.Context, host string, port string, link string, udp bool, realm string, peer net.Addr, conn2 net.PacketConn, c chan<- error) { +type connectedUDPConn struct { + *net.UDPConn +} + +func (c *connectedUDPConn) WriteTo(p []byte, _ net.Addr) (int, error) { + return c.Write(p) +} + +type turnParams struct { + host string + port string + link string + udp bool + getCreds getCredsFunc +} + +func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, conn2 net.PacketConn, c chan<- error) { var err error = nil defer func() { c <- err }() - user, pass, url, err1 := getCreds(link) + user, pass, url, err1 := turnParams.getCreds(turnParams.link) if err1 != nil { err = fmt.Errorf("failed to get TURN credentials: %s", err1) return } - var turnServerAddr string - if host == "" || port == "" { - turnServerAddr = url[5:] - } else { - turnServerAddr = net.JoinHostPort(host, port) + urlhost, urlport, err1 := net.SplitHostPort(url) + if err1 != nil { + err = fmt.Errorf("failed to parse TURN server address: %s", err1) + return + } + if turnParams.host != "" { + urlhost = turnParams.host } + if turnParams.port != "" { + urlport = turnParams.port + } + var turnServerAddr string + turnServerAddr = net.JoinHostPort(urlhost, urlport) turnServerUdpAddr, err1 := net.ResolveUDPAddr("udp", turnServerAddr) if err1 != nil { err = fmt.Errorf("failed to resolve TURN server address: %s", err1) @@ -298,20 +611,21 @@ func oneTurnConnection(ctx context.Context, host string, port string, link strin var d net.Dialer ctx1, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - if udp { - turnConn, err1 = net.ListenPacket("udp", "") // nolint: noctx - if err1 != nil { - err = fmt.Errorf("failed to connect to TURN server: %s", err1) + if turnParams.udp { + conn, err2 := net.DialUDP("udp4", nil, turnServerUdpAddr) // nolint: noctx + if err2 != nil { + err = fmt.Errorf("failed to connect to TURN server: %s", err2) return } defer func() { - if err1 = turnConn.Close(); err1 != nil { + if err1 = conn.Close(); err1 != nil { err = fmt.Errorf("failed to close TURN server connection: %s", err1) return } }() + turnConn = &connectedUDPConn{conn} } else { - conn, err2 := d.DialContext(ctx1, "tcp", turnServerAddr) // nolint: noctx + conn, err2 := d.DialContext(ctx1, "tcp4", turnServerAddr) // nolint: noctx if err2 != nil { err = fmt.Errorf("failed to connect to TURN server: %s", err2) return @@ -332,7 +646,6 @@ func oneTurnConnection(ctx context.Context, host string, port string, link strin Conn: turnConn, Username: user, Password: pass, - Realm: realm, LoggerFactory: logging.NewDefaultLoggerFactory(), } @@ -375,6 +688,7 @@ func oneTurnConnection(ctx context.Context, host string, port string, link strin relayConn.SetDeadline(time.Now()) conn2.SetDeadline(time.Now()) }) + var addr atomic.Value // Start read-loop on conn2 (output of DTLS) go func() { defer wg.Done() @@ -386,12 +700,14 @@ func oneTurnConnection(ctx context.Context, host string, port string, link strin return default: } - n, _, err1 := conn2.ReadFrom(buf) + n, addr1, err1 := conn2.ReadFrom(buf) if err1 != nil { log.Printf("Failed: %s", err1) return } + addr.Store(addr1) // store peer + _, err1 = relayConn.WriteTo(buf[:n], peer) if err1 != nil { log.Printf("Failed: %s", err1) @@ -416,8 +732,13 @@ func oneTurnConnection(ctx context.Context, host string, port string, link strin log.Printf("Failed: %s", err1) return } + addr1, ok := addr.Load().(net.Addr) + if !ok { + log.Printf("Failed: no listener ip") + return + } - _, err1 = conn2.WriteTo(buf[:n], nil) + _, err1 = conn2.WriteTo(buf[:n], addr1) if err1 != nil { log.Printf("Failed: %s", err1) return @@ -430,6 +751,40 @@ func oneTurnConnection(ctx context.Context, host string, port string, link strin conn2.SetDeadline(time.Time{}) } +func oneDtlsConnectionLoop(ctx context.Context, peer *net.UDPAddr, listenConnChan <-chan net.PacketConn, connchan chan<- net.PacketConn, okchan chan<- struct{}) { + 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) + } + } + } +} + +func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, connchan <-chan net.PacketConn, t <-chan time.Time) { + for { + select { + case <-ctx.Done(): + return + case conn2 := <-connchan: + select { + case <-t: + c := make(chan error) + go oneTurnConnection(ctx, turnParams, peer, conn2, c) + if err := <-c; err != nil { + log.Printf("%s", err) + } + default: + } + } + } +} + func main() { //nolint:cyclop ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -447,99 +802,101 @@ func main() { //nolint:cyclop }() host := flag.String("turn", "", "override TURN server ip") - port := flag.String("port", "19302", "override TURN port") + port := flag.String("port", "", "override TURN port") listen := flag.String("listen", "127.0.0.1:9000", "listen on ip:port") - realm := flag.String("realm", "call6-7.vkuser.net", "TURN Realm") - link := flag.String("link", "", "VK calls invite link \"https://vk.com/call/join/...\"") + vklink := flag.String("vk-link", "", "VK calls invite link \"https://vk.com/call/join/...\"") + yalink := flag.String("yandex-link", "", "Yandex telemost invite link \"https://telemost.yandex.ru/j/...\"") peerAddr := flag.String("peer", "", "peer server address (host:port)") - n := flag.Int("n", 16, "connections to TURN") + n := flag.Int("n", 0, "connections to TURN (default 16 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") flag.Parse() if *peerAddr == "" { log.Panicf("Need peer address!") } - if *link == "" { - log.Panicf("Need invite link!") - } peer, err := net.ResolveUDPAddr("udp", *peerAddr) if err != nil { panic(err) } + if (*vklink == "") == (*yalink == "") { + log.Panicf("Need either vk-link or yandex-link!") + } + var link string + var getCreds getCredsFunc + if *vklink != "" { + link = (*vklink)[len(*vklink)-43:] + getCreds = getVkCreds + if *n <= 0 { + *n = 16 + } + } else { + link = (*yalink)[len(*yalink)-10:] + getCreds = getYandexCreds + if *n <= 0 { + *n = 1 + } + } + params := &turnParams{ + *host, + *port, + link, + *udp, + getCreds, + } - *link = (*link)[len(*link)-43:] - + listenConnChan := make(chan net.PacketConn) listenConn, err := net.ListenPacket("udp", *listen) // nolint: noctx if err != nil { log.Panicf("Failed to listen: %s", err) } - defer func() { + context.AfterFunc(ctx, func() { if closeErr := listenConn.Close(); closeErr != nil { log.Panicf("Failed to close local connection: %s", closeErr) } - }() - - okchan := make(chan struct{}) - connchan := make(chan net.PacketConn) - - wg1 := sync.WaitGroup{} - wg1.Go(func() { + }) + go func() { for { - c1 := make(chan error) - go oneDtlsConnection(ctx, peer, listenConn, connchan, okchan, c1) - if err := <-c1; err != nil { - log.Printf("%s", err) - } select { case <-ctx.Done(): return - default: + case listenConnChan <- listenConn: } } - }) + }() + wg1 := sync.WaitGroup{} t := time.Tick(100 * time.Millisecond) - wg1.Go(func() { - for { - select { - case <-ctx.Done(): - return - case conn2 := <-connchan: - select { - case <-t: - c := make(chan error) - go oneTurnConnection(ctx, *host, *port, *link, *udp, *realm, peer, conn2, c) - if err := <-c; err != nil { - log.Printf("%s", err) - } - default: - } - } + if *direct { + for i := 0; i < *n; i++ { + wg1.Go(func() { + oneTurnConnectionLoop(ctx, params, peer, listenConnChan, t) + }) } - }) + } else { + okchan := make(chan struct{}) + connchan := make(chan net.PacketConn) - for i := 0; i < *n-1; i++ { wg1.Go(func() { - for { - select { - case <-ctx.Done(): - return - case <-okchan: - select { - case conn2 := <-connchan: - select { - case <-t: - c2 := make(chan error) - go oneTurnConnection(ctx, *host, *port, *link, *udp, *realm, peer, conn2, c2) - if err := <-c2; err != nil { - log.Printf("%s", err) - } - default: - } - default: - } - } - } + oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, okchan) }) + + wg1.Go(func() { + 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.Go(func() { + oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, nil) + }) + wg1.Go(func() { + oneTurnConnectionLoop(ctx, params, peer, connchan, t) + }) + } } wg1.Wait() diff --git a/go.mod b/go.mod index e271175..57582dd 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,10 @@ go 1.25.5 require ( 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.10 github.com/pion/logging v0.2.4 - github.com/pion/turn/v4 v4.1.4 + github.com/pion/turn/v5 v5.0.2 ) require ( @@ -15,6 +16,6 @@ require ( github.com/pion/stun/v3 v3.1.1 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/wlynxg/anet v0.0.5 // indirect - golang.org/x/crypto v0.46.0 // indirect + golang.org/x/crypto v0.47.0 // indirect golang.org/x/sys v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index d77591a..92594c3 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg= github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= @@ -14,18 +16,18 @@ github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= -github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ= -github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ= +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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +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/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=