Browse Source

Add yandex mode

Add direct mode
Make separate dtls session for each connection
Refactor
pull/14/head v1.1.0
cacggghp 4 months ago
committed by GitHub
parent
commit
b63eba3cb4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 91
      README.md
  2. 583
      client/main.go
  3. 5
      go.mod
  4. 14
      go.sum

91
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 <ip сервера wg>:56000 -link <VK ссылка> -listen 127.0.0.1:9000`
```
termux-wake-lock
```
Телефон не будет уходить в глубокий сон, так что на ночь ставьте на зарядку. Чтобы отключить:
```
termux-wake-unlock
```
Копируем бинарник в локальную папку, даём права на исполнение:
```
cp /sdcard/Download/client-android ./
chmod 777 ./client-android
```
Запускаем:
```
./client-android -peer <ip сервера wg>:56000 -vk-link <VK ссылка> -listen 127.0.0.1:9000
```
Или
```
./client-android -udp -turn 5.255.211.241 -peer <ip сервера wg>:56000 -yandex-link <Ya ссылка> -listen 127.0.0.1:9000
```
**Если после включения VPN в терминале вылезают ошибки DNS, попробуйте в Wireguard включить VPN только для нужных приложений.**
#### Linux
@ -29,7 +45,13 @@
Скрипт будет добавлять маршруты к нужным ip:
`./client-linux -peer <ip сервера wg>:56000 -link <VK ссылка> -listen 127.0.0.1:9000 | sudo routes.sh`
```
./client-linux -peer <ip сервера wg>:56000 -vk-link <VK ссылка> -listen 127.0.0.1:9000 | sudo routes.sh
```
```
./client-linux -udp -turn 5.255.211.241 -peer <ip сервера wg>:56000 -yandex-link <Ya ссылка> -listen 127.0.0.1:9000 | sudo routes.sh
```
Не включайте впн, пока программа не установит соединение! В отличие от андроида, здесь часть запросов будет идти через впн (dns и запрос подключения к turn)
#### Windows
@ -37,15 +59,43 @@
В PowerShell от Администратора (чтобы скрипт прописывал маршруты):
`./client.exe -peer <ip сервера wg>:56000 -link <VK ссылка> -listen 127.0.0.1:9000 | routes.ps1`
```
./client.exe -peer <ip сервера wg>:56000 -vk-link <VK ссылка> -listen 127.0.0.1:9000 | routes.ps1
```
```
./client.exe -udp -turn 5.255.211.241 -peer <ip сервера wg>:56000 -yandex-link <Ya ссылка> -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`
<details>
<summary>
Рабочие IP
</summary>
5.255.211.241
5.255.211.242
5.255.211.243
5.255.211.245
5.255.211.246
</details>
Спасибо https://github.com/KillTheCensorship/Turnel за часть кода :)
## v2ray
@ -153,4 +203,7 @@
}
```
</details>
</details>
## Direct mode
С флагом `-no-dtls` можно отправлять пакеты без обфускации DTLS и подключаться к обычным серверам Wireguard. Может привести к бану от вк/яндекса.

583
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()

5
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
)

14
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=

Loading…
Cancel
Save