Files
free-turn-proxy/internal/proxy/common/common.go
T
samosvalishe 40000fde5d feat(turn): фоллбэк по нескольким relay-адресам при allocate
VK отдаёт несколько relay-адресов, но стрим использовал ровно один
(streamID%n). Если этот IP душит DPI/RST — allocate падает без альтернативы.

Credentials несёт список адресов (primary-first по streamID), DialTURN
пробует кандидатов по очереди и берёт первый успешный allocate. Балансировка
relay-IP по стримам сохранена, остальные адреса — фоллбэк. HostOverride (-turn)
схлопывает список до одного адреса.
2026-06-08 00:09:17 +03:00

67 lines
3.0 KiB
Go

// Package common содержит хелперы, общие для udprelay и tcpfwd
// (TURN-dial + создание obf-кодека). Два режима прокси по-разному компонуют DTLS
// и rtpopus, поэтому полная абстракция Engine/Handler намеренно не вводится —
// пакет собирает только действительно идентичный код.
package common
import (
"context"
"errors"
"fmt"
"net"
"github.com/samosvalishe/free-turn-proxy/internal/transport/turndial"
"github.com/samosvalishe/free-turn-proxy/internal/wire/rtpopus"
)
// GetCredsFunc разрешает TURN-реквизиты для streamID. Реализуется provider'ом
// (см. internal/provider): provider держит идентификатор сессии (link/room/key)
// внутри, pipeline передаёт только streamID. rawURLs — кандидаты host:port в
// порядке предпочтения.
type GetCredsFunc func(ctx context.Context, streamID int) (user, pass string, rawURLs []string, err error)
// DialTURN получает реквизиты и открывает TURN-поток, пробуя кандидатов по
// очереди: если allocate на первом не проходит (DPI-дроп/RST на relay-IP),
// берёт следующий. Возвращает первый успешный Stream. Вызывающий отвечает за
// закрытие потока и политику retry при auth-ошибке (udprelay) или перезапуска
// сессии (tcpfwd).
func DialTURN(ctx context.Context, host, port string, udp bool, peer *net.UDPAddr, streamID int, getCreds GetCredsFunc) (*turndial.Stream, error) {
user, pass, rawURLs, err := getCreds(ctx, streamID)
if err != nil {
return nil, fmt.Errorf("get TURN creds: %w", err)
}
if len(rawURLs) == 0 {
return nil, fmt.Errorf("no TURN candidates")
}
// HostOverride (-turn) принудительно задаёт host → все кандидаты резолвятся
// в одну цель, гонять их нет смысла; пробуем только первого.
if host != "" {
rawURLs = rawURLs[:1]
}
var errs []error
for _, rawURL := range rawURLs {
stream, derr := turndial.Open(ctx, turndial.Config{
HostOverride: host,
PortOverride: port,
TransportUDP: udp,
}, peer, user, pass, rawURL)
if derr == nil {
return stream, nil
}
errs = append(errs, fmt.Errorf("%s: %w", rawURL, derr))
if ctx.Err() != nil {
break
}
}
return nil, fmt.Errorf("all TURN candidates failed: %w", errors.Join(errs...))
}
// NewClientObf возвращает клиентский rtpopus.Conn если key нужной длины,
// иначе (nil, nil) — обфускация отключена. Ошибки NewConn пробрасываются вызывающему.
func NewClientObf(key []byte) (*rtpopus.Conn, error) {
if len(key) != rtpopus.KeyLen {
return nil, nil
}
return rtpopus.NewConn(key, false)
}