mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
380 lines
12 KiB
Go
380 lines
12 KiB
Go
package udprelay
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/cbeuw/connutil"
|
|
"github.com/samosvalishe/free-turn-proxy/internal/clientsdb"
|
|
"github.com/samosvalishe/free-turn-proxy/internal/provider"
|
|
"github.com/samosvalishe/free-turn-proxy/internal/randx"
|
|
"github.com/samosvalishe/free-turn-proxy/internal/transport/turndial"
|
|
"github.com/samosvalishe/free-turn-proxy/internal/wire"
|
|
"github.com/samosvalishe/free-turn-proxy/internal/wire/shape"
|
|
)
|
|
|
|
// errPairRecycled - пару свернул TURN-цикл (аллокация мертва), а не сеть: сетевой
|
|
// backoff тут только удлиняет простой.
|
|
var errPairRecycled = errors.New("udprelay: stream pair recycled")
|
|
|
|
const (
|
|
pipeBufLimit = 256 << 10
|
|
// maxDatagramLen - бюджет локальной датаграммы (WG/AWG) по всей цепочке, как у сервера.
|
|
maxDatagramLen = 2048
|
|
// maxRecordLen - та же датаграмма в DTLS-записи: заголовок с CID, явный nonce, тег GCM.
|
|
maxRecordLen = maxDatagramLen + 64
|
|
)
|
|
|
|
// streamPair связывает DTLS-сессию с аллокацией, поверх которой она поднята. Смерть
|
|
// аллокации обязана ронять DTLS: у новой аллокации другой relayed-адрес, а миграция
|
|
// адреса по DTLS Connection ID под obf-профилем на сервере не работает - сервер примет
|
|
// такие записи за новую сессию и будет ждать от них handshake.
|
|
type streamPair struct {
|
|
pipe net.PacketConn
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// DTLSLoop поддерживает и перезапускает DTLS-соединение для указанного streamID.
|
|
func DTLSLoop(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, listenConn net.PacketConn, inboundChan <-chan *Packet, connchan chan<- streamPair, okchan chan<- struct{}, streamID int) {
|
|
for ctx.Err() == nil {
|
|
err := oneDTLS(ctx, deps, params, peer, listenConn, inboundChan, connchan, okchan, streamID)
|
|
var wait time.Duration
|
|
switch {
|
|
case err == nil, errors.Is(err, errPairRecycled):
|
|
// Пара пересоздаётся под новую аллокацию - TURN-цикл уже держит свою паузу.
|
|
continue
|
|
case time.Now().Unix() < deps.Auth.BackoffUntilUnix() && errors.Is(err, context.DeadlineExceeded):
|
|
wait = time.Duration(1+randx.Intn(2)) * time.Second
|
|
default:
|
|
wait = time.Duration(10+randx.Intn(20)) * time.Second
|
|
deps.log().Warnf("[STREAM %d] DTLS: %v - повтор через %s", streamID, err, wait)
|
|
}
|
|
if !sleepCtx(ctx, wait) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// TURNLoop управляет жизненным циклом одной TURN-аллокации.
|
|
func TURNLoop(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, connchan <-chan streamPair, streamID int) {
|
|
for {
|
|
var pair streamPair
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case pair = <-connchan:
|
|
}
|
|
c := make(chan error, 1)
|
|
go deps.guard(func() { oneTURN(ctx, deps, params, peer, pair.pipe, streamID, c) })()
|
|
|
|
var err error
|
|
select {
|
|
case err = <-c:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
// Аллокация кончилась - DTLS поверх неё сервер больше не адресует (см. streamPair).
|
|
pair.cancel()
|
|
if err == nil {
|
|
continue
|
|
}
|
|
if errors.Is(err, provider.ErrFatalNoStreams) {
|
|
deps.log().Errorf("[STREAM %d] Fatal provider error. Shutting down application.", streamID)
|
|
deps.fatal(err)
|
|
return
|
|
}
|
|
if !sleepCtx(ctx, turnRetryDelay(deps, streamID, err)) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// turnRetryDelay: пауза провайдера важнее собственной - ретрай в её середине только
|
|
// продлевает локаут. Задержки свои, не tcprelay: UDP-пара дешевле TCP-стека.
|
|
func turnRetryDelay(deps *Deps, streamID int, err error) time.Duration {
|
|
switch {
|
|
case errors.Is(err, turndial.ErrAllocQuota):
|
|
wait := turndial.QuotaBackoff()
|
|
deps.log().Warnf("[STREAM %d] квота аллокаций занята - пауза %s", streamID, wait)
|
|
return wait
|
|
case errors.Is(err, provider.ErrBackoffActive):
|
|
lockoutEnd := deps.Auth.BackoffUntilUnix()
|
|
if lockoutEnd <= 0 {
|
|
deps.log().Warnf("[STREAM %d] Backing off for 60 seconds (provider requests wait)", streamID)
|
|
return 60 * time.Second
|
|
}
|
|
if d := time.Until(time.Unix(lockoutEnd, 0)); d > 0 {
|
|
return d
|
|
}
|
|
return 5 * time.Second
|
|
default:
|
|
deps.log().Errorf("[STREAM %d] %s", streamID, err)
|
|
return 2 * time.Second
|
|
}
|
|
}
|
|
|
|
// sleepCtx: false - ctx отменён раньше.
|
|
func sleepCtx(ctx context.Context, d time.Duration) bool {
|
|
t := time.NewTimer(d)
|
|
defer t.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-t.C:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func oneDTLS(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, listenConn net.PacketConn, inboundChan <-chan *Packet, connchan chan<- streamPair, okchan chan<- struct{}, streamID int) error {
|
|
dtlsctx, dtlscancel := context.WithCancel(ctx)
|
|
defer dtlscancel()
|
|
|
|
err := dtlsSession(dtlsctx, dtlscancel, deps, params, peer, listenConn, inboundChan, connchan, okchan, streamID)
|
|
// Отменена именно пара, а не вся сессия - значит её свернул TURN-цикл.
|
|
if err != nil && ctx.Err() == nil && dtlsctx.Err() != nil {
|
|
return errPairRecycled
|
|
}
|
|
return err
|
|
}
|
|
|
|
func dtlsSession(dtlsctx context.Context, dtlscancel context.CancelFunc, deps *Deps, params *Params, peer *net.UDPAddr, listenConn net.PacketConn, inboundChan <-chan *Packet, connchan chan<- streamPair, okchan chan<- struct{}, streamID int) error {
|
|
if !sleepCtx(dtlsctx, time.Duration(randx.Intn(400)+100)*time.Millisecond) {
|
|
return dtlsctx.Err()
|
|
}
|
|
|
|
conn1, conn2 := connutil.LimitedAsyncPacketPipe(pipeBufLimit)
|
|
defer func() { _ = conn1.Close() }()
|
|
defer func() { _ = conn2.Close() }()
|
|
// Ровно один раз: пара строго 1:1, иначе следующая аллокация села бы на DTLS, который
|
|
// уже сворачивают. Отдаём до handshake - его пакеты идут через эту же аллокацию.
|
|
select {
|
|
case connchan <- streamPair{pipe: conn2, cancel: dtlscancel}:
|
|
case <-dtlsctx.Done():
|
|
return dtlsctx.Err()
|
|
}
|
|
dtlsConn, err := deps.DTLSDialer.Dial(dtlsctx, conn1, peer)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect DTLS: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = dtlsConn.Close()
|
|
deps.log().Debugf("[STREAM %d] Closed DTLS connection", streamID)
|
|
}()
|
|
deps.log().Debugf("[STREAM %d] Established DTLS connection", streamID)
|
|
|
|
if err := clientsdb.WriteClientID(dtlsctx, dtlsConn, params.ClientID, clientsdb.ModeUDP); err != nil {
|
|
return fmt.Errorf("failed to write client ID: %w", err)
|
|
}
|
|
// Аллокация без DTLS ещё не может передавать пользовательский трафик.
|
|
deps.ConnectedStreams.Add(1)
|
|
defer deps.ConnectedStreams.Add(-1)
|
|
if okchan != nil {
|
|
select {
|
|
case okchan <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
forwardDone := make(chan struct{})
|
|
go func() {
|
|
defer close(forwardDone)
|
|
forwardToLocal(dtlsConn, listenConn, deps.ActiveLocalPeer)
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-dtlsctx.Done():
|
|
return dtlsctx.Err()
|
|
case <-forwardDone:
|
|
return errors.New("DTLS connection closed by remote peer")
|
|
case pkt := <-inboundChan:
|
|
_, err := dtlsConn.Write(pkt.Data[:pkt.N])
|
|
packetPool.Put(pkt)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to forward packet to DTLS: %w", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func forwardToLocal(dtlsConn net.Conn, listenConn net.PacketConn, activeLocalPeer *atomic.Value) {
|
|
var buf [maxDatagramLen]byte
|
|
for {
|
|
n, err := dtlsConn.Read(buf[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
addr, ok := activeLocalPeer.Load().(net.Addr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if _, err := listenConn.WriteTo(buf[:n], addr); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, conn2 net.PacketConn, streamID int, c chan<- error) {
|
|
var err error
|
|
defer func() {
|
|
c <- err
|
|
}()
|
|
|
|
codec, err := wire.NewClientCodec(params.Profile, params.ObfKey)
|
|
if err != nil {
|
|
err = fmt.Errorf("OBF init: %w", err)
|
|
return
|
|
}
|
|
stream, derr := params.Dial(ctx, streamID)
|
|
if derr != nil {
|
|
if deps.Auth.IsAuthError(derr) {
|
|
deps.Auth.HandleAuthError(streamID)
|
|
}
|
|
err = fmt.Errorf("connect to TURN server: %w", derr)
|
|
return
|
|
}
|
|
relayConn := stream.Relay
|
|
if deps.OnTURNServer != nil {
|
|
deps.OnTURNServer(stream.ServerUDPAddr.IP)
|
|
}
|
|
if params.ObfTiming > 0 {
|
|
relayConn = shape.WrapPacketConn(relayConn, params.ObfTiming)
|
|
deps.log().Debugf("[STREAM %d] obf-timing=%s", streamID, params.ObfTiming)
|
|
}
|
|
|
|
deps.Auth.ResetErrors(streamID)
|
|
|
|
relayedAddr := relayConn.LocalAddr().String()
|
|
deps.log().Infof("[STREAM %d] TURN allocation up: relayed=%s server=%s",
|
|
streamID, relayedAddr, stream.ServerUDPAddr.IP)
|
|
defer releaseStream(deps, stream, relayedAddr, streamID)
|
|
|
|
turnctx, turncancel := context.WithCancel(ctx)
|
|
defer turncancel()
|
|
// без дедлайна relayConn.ReadFrom не проснётся на отмене turnctx - wg.Wait встанет намертво
|
|
context.AfterFunc(turnctx, func() {
|
|
if err := relayConn.SetDeadline(time.Now()); err != nil {
|
|
deps.log().Errorf("[STREAM %d] Failed to set relay deadline: %s", streamID, err)
|
|
}
|
|
})
|
|
|
|
var pipeAddr atomic.Value
|
|
var wg sync.WaitGroup
|
|
wg.Go(func() { recycleOnPermDead(turnctx, turncancel, deps, stream.PermDead, conn2, streamID) })
|
|
wg.Go(func() {
|
|
defer turncancel()
|
|
relayUplink(turnctx, deps, params, codec, conn2, relayConn, peer, &pipeAddr, streamID)
|
|
})
|
|
wg.Go(func() {
|
|
defer turncancel()
|
|
relayDownlink(deps, params, codec, conn2, relayConn, &pipeAddr, streamID)
|
|
})
|
|
wg.Wait()
|
|
|
|
if err := relayConn.SetDeadline(time.Time{}); err != nil {
|
|
deps.log().Errorf("Failed to clear relay deadline: %s", err)
|
|
}
|
|
if err := conn2.SetDeadline(time.Time{}); err != nil {
|
|
deps.log().Errorf("Failed to clear pipe deadline: %s", err)
|
|
}
|
|
}
|
|
|
|
func releaseStream(deps *Deps, stream *turndial.Stream, relayedAddr string, streamID int) {
|
|
cerr := stream.Close()
|
|
deps.log().Infof("[STREAM %d] TURN allocation released: relayed=%s deallocate=%v",
|
|
streamID, relayedAddr, cerr)
|
|
if cerr != nil {
|
|
deps.Auth.DropCredentials(streamID)
|
|
}
|
|
}
|
|
|
|
func recycleOnPermDead(ctx context.Context, cancel context.CancelFunc, deps *Deps, permDead <-chan struct{}, conn2 net.PacketConn, streamID int) {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-permDead:
|
|
deps.log().Warnf("[STREAM %d] TURN refresh failed - recycle allocation", streamID)
|
|
cancel()
|
|
}
|
|
if err := conn2.SetDeadline(time.Now()); err != nil {
|
|
deps.log().Errorf("[STREAM %d] Failed to set pipe deadline: %s", streamID, err)
|
|
}
|
|
}
|
|
|
|
func relayUplink(ctx context.Context, deps *Deps, params *Params, codec wire.Codec, conn2, relayConn net.PacketConn, peer net.Addr, pipeAddr *atomic.Value, streamID int) {
|
|
var buf, readSlot []byte
|
|
if codec != nil {
|
|
buf = make([]byte, codec.MaxWire(maxRecordLen))
|
|
readSlot = buf[codec.HeaderLen() : codec.HeaderLen()+maxRecordLen]
|
|
} else {
|
|
buf = make([]byte, maxRecordLen)
|
|
readSlot = buf
|
|
}
|
|
addrStored := false
|
|
for ctx.Err() == nil {
|
|
n, addr, err := conn2.ReadFrom(readSlot)
|
|
if err != nil || ctx.Err() != nil {
|
|
return
|
|
}
|
|
if !addrStored {
|
|
pipeAddr.Store(addr)
|
|
addrStored = true
|
|
}
|
|
|
|
out := readSlot[:n]
|
|
if codec != nil {
|
|
written, wErr := codec.WrapInPlace(buf, n)
|
|
if wErr != nil {
|
|
deps.log().Errorf("[STREAM %d] OBF wrap failed: %v", streamID, wErr)
|
|
return
|
|
}
|
|
out = buf[:written]
|
|
}
|
|
|
|
written, err := relayConn.WriteTo(out, peer)
|
|
if params.TrafficStats != nil {
|
|
params.TrafficStats.AddTx(written)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func relayDownlink(deps *Deps, params *Params, codec wire.Codec, conn2, relayConn net.PacketConn, pipeAddr *atomic.Value, streamID int) {
|
|
readBufLen := maxRecordLen
|
|
if codec != nil {
|
|
readBufLen = codec.MaxWire(maxRecordLen)
|
|
}
|
|
buf := make([]byte, readBufLen)
|
|
for {
|
|
n, _, err := relayConn.ReadFrom(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
addr, ok := pipeAddr.Load().(net.Addr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
payload := buf[:n]
|
|
if codec != nil {
|
|
p, uErr := codec.UnwrapInPlace(payload)
|
|
if uErr != nil {
|
|
deps.log().Errorf("[STREAM %d] OBF unwrap failed: %v (n=%d)", streamID, uErr, n)
|
|
continue
|
|
}
|
|
payload = p
|
|
}
|
|
if params.TrafficStats != nil {
|
|
params.TrafficStats.AddRx(len(payload))
|
|
}
|
|
if _, err := conn2.WriteTo(payload, addr); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|