Files
2026-09-22 22:33:56 +03:00

104 lines
3.3 KiB
Go

// Package tcpserver - серверная сторона tcp-режима: KCP+smux поверх DTLS, каждый
// smux-поток форвардится в локальный TCP backend.
package tcpserver
import (
"context"
"errors"
"net"
"sync"
"time"
"github.com/samosvalishe/free-turn-proxy/internal/logx"
"github.com/samosvalishe/free-turn-proxy/internal/netconn"
"github.com/samosvalishe/free-turn-proxy/internal/proxy/bond"
"github.com/samosvalishe/free-turn-proxy/internal/safego"
"github.com/samosvalishe/free-turn-proxy/internal/transport/kcpmux"
"github.com/xtaci/smux"
)
const backendDialTimeout = 10 * time.Second
// Handle блокирует вызывающую горутину до закрытия сессии клиентом или ctx.
func Handle(ctx context.Context, logger logx.Logger, dtlsConn net.Conn, connectAddr string, profile kcpmux.Profile) {
handle(ctx, logger, dtlsConn, profile, func(ctx context.Context, stream *smux.Stream) {
handleStream(ctx, logger, stream, connectAddr)
})
}
func HandleBond(ctx context.Context, logger logx.Logger, dtlsConn net.Conn, connectAddr string, profile kcpmux.Profile, server *bond.Server, clientID string) {
handle(ctx, logger, dtlsConn, profile, func(ctx context.Context, stream *smux.Stream) {
if err := server.Handle(ctx, stream, clientID, connectAddr); err != nil && ctx.Err() == nil {
logger.Debugf("tcpserver bond: %v", err)
}
})
}
func handle(ctx context.Context, logger logx.Logger, dtlsConn net.Conn, profile kcpmux.Profile, streamHandler func(context.Context, *smux.Stream)) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
kcpSess, err := kcpmux.Accept(dtlsConn, profile)
if err != nil {
logger.Errorf("tcpserver: %s", err)
return
}
defer func() {
if closeErr := kcpSess.Close(); closeErr != nil {
logger.Warnf("tcpserver: close KCP session: %v", closeErr)
}
}()
smuxSess, err := smux.Server(kcpSess, kcpmux.SmuxConfig())
if err != nil {
logger.Errorf("tcpserver: smux server: %s", err)
return
}
defer func() {
if closeErr := smuxSess.Close(); closeErr != nil {
logger.Warnf("tcpserver: close smux session: %v", closeErr)
}
}()
logger.Debugf("tcpserver: smux session established")
// ctx живёт всё время процесса - без stop() хук копился бы на каждую сессию.
stopOnCancel := context.AfterFunc(ctx, func() { _ = smuxSess.Close() })
defer stopOnCancel()
var wg sync.WaitGroup
for {
stream, err := smuxSess.AcceptStream()
if err != nil {
if ctx.Err() == nil {
logger.Debugf("tcpserver: smux accept: %s", err)
}
break
}
wg.Go(func() {
_ = safego.Run(logger, func() { streamHandler(ctx, stream) })
})
}
cancel()
wg.Wait()
}
func handleStream(ctx context.Context, logger logx.Logger, s *smux.Stream, connectAddr string) {
defer func() {
if err := s.Close(); err != nil && !errors.Is(err, smux.ErrGoAway) {
logger.Warnf("tcpserver: close smux stream: %v", err)
}
}()
backend, err := (&net.Dialer{Timeout: backendDialTimeout}).DialContext(ctx, "tcp", connectAddr)
if err != nil {
logger.Errorf("tcpserver: backend dial %s: %s", connectAddr, err)
return
}
defer func() {
if closeErr := backend.Close(); closeErr != nil {
logger.Warnf("tcpserver: close backend connection: %v", closeErr)
}
}()
netconn.BiCopy(ctx, s, backend, logger.Debugf)
}