diff --git a/README.md b/README.md index 4bc5e65..cd17871 100644 --- a/README.md +++ b/README.md @@ -227,5 +227,120 @@ chmod 777 ./client-android +## VLESS (TCP-режим) + +Если WireGuard блокируется DPI, можно использовать VLESS через флаг `-tcp`. В этом режиме вместо UDP-пакетов пробрасываются TCP-соединения через TURN-туннель с помощью KCP и smux. + +### Настройка +1. На VPS установить Xray с VLESS inbound +2. Запустить `server` с флагом `-tcp` +3. На клиенте запустить `client` с флагом `-tcp` +4. Настроить Xray/v2rayN клиент с VLESS outbound на `127.0.0.1:9000` + +### Сервер (VPS) +``` +./server -listen 0.0.0.0:56000 -connect 127.0.0.1:443 -tcp +``` + +#### Docker +``` +docker run -p 56000:56000/udp -e CONNECT_ADDR=127.0.0.1:443 -e TCP_MODE=true vk-turn-proxy +``` + +### Клиент +``` +./client -peer :56000 -vk-link -listen 127.0.0.1:9000 -tcp +``` + +
+ + +Xray клиент (config.json) + + +```json +{ + "inbounds": [ + { + "protocol": "socks", + "listen": "127.0.0.1", + "port": 1080, + "settings": { + "udp": true + }, + "sniffing": { + "enabled": true, + "destOverride": ["http", "tls"] + } + } + ], + "outbounds": [ + { + "protocol": "vless", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 9000, + "users": [ + { + "id": "", + "encryption": "none" + } + ] + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "none" + } + } + ] +} +``` + +
+ +
+ + +Xray сервер (config.json) + + +```json +{ + "inbounds": [ + { + "protocol": "vless", + "listen": "127.0.0.1", + "port": 443, + "settings": { + "clients": [ + { + "id": "<тот же UUID>", + "level": 0 + } + ], + "decryption": "none" + } + } + ], + "outbounds": [ + { + "protocol": "freedom", + "settings": { + "domainStrategy": "UseIPv4" + } + } + ] +} +``` + +
+ +> **Важно:** В TCP-режиме используется один TURN-поток. Для VK это ограничивает скорость ~5 Мбит/с. + ## Direct mode С флагом `-no-dtls` можно отправлять пакеты без обфускации DTLS и подключаться к обычным серверам Wireguard. Может привести к бану от вк/яндекса. + diff --git a/client/main.go b/client/main.go index 776b1a6..58e347b 100644 --- a/client/main.go +++ b/client/main.go @@ -10,14 +10,6 @@ import ( "encoding/json" "flag" "fmt" - "github.com/bschaatsbergen/dnsdialer" - "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/v5" "io" "log" "net" @@ -29,6 +21,17 @@ import ( "sync/atomic" "syscall" "time" + + "github.com/bschaatsbergen/dnsdialer" + "github.com/cacggghp/vk-turn-proxy/tcputil" + "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/v5" + "github.com/xtaci/smux" ) type getCredsFunc func(string) (string, string, string, error) @@ -830,6 +833,7 @@ func main() { //nolint:cyclop 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") + tcpMode := flag.Bool("tcp", false, "TCP mode: forward TCP connections (for VLESS) instead of UDP packets") flag.Parse() if *peerAddr == "" { log.Panicf("Need peer address!") @@ -879,6 +883,11 @@ func main() { //nolint:cyclop getCreds, } + if *tcpMode { + runTCPMode(ctx, params, peer, *listen) + return + } + listenConnChan := make(chan net.PacketConn) listenConn, err := net.ListenPacket("udp", *listen) // nolint: noctx if err != nil { @@ -936,3 +945,238 @@ func main() { //nolint:cyclop wg1.Wait() } + +// runTCPMode implements TCP forwarding mode for VLESS. +// It establishes a DTLS tunnel through TURN, then creates a KCP+smux session +// on top, and forwards incoming TCP connections as smux streams. +func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAddr string) { + for { + select { + case <-ctx.Done(): + return + default: + } + + err := runTCPSession(ctx, tp, peer, listenAddr) + if err != nil { + log.Printf("TCP session error: %s, reconnecting...", err) + } + + select { + case <-ctx.Done(): + return + case <-time.After(2 * time.Second): + } + } +} + +func runTCPSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAddr string) error { + // 1. Get TURN credentials + user, pass, url, err := tp.getCreds(tp.link) + if err != nil { + return fmt.Errorf("get TURN creds: %w", err) + } + urlhost, urlport, err := net.SplitHostPort(url) + if err != nil { + return fmt.Errorf("parse TURN addr: %w", err) + } + if tp.host != "" { + urlhost = tp.host + } + if tp.port != "" { + urlport = tp.port + } + turnServerAddr := net.JoinHostPort(urlhost, urlport) + turnServerUdpAddr, err := net.ResolveUDPAddr("udp", turnServerAddr) + if err != nil { + return fmt.Errorf("resolve TURN addr: %w", err) + } + turnServerAddr = turnServerUdpAddr.String() + fmt.Println(turnServerUdpAddr.IP) + + // 2. Connect to TURN server + var turnConn net.PacketConn + ctx1, cancel1 := context.WithTimeout(ctx, 5*time.Second) + defer cancel1() + if tp.udp { + conn, err := net.DialUDP("udp", nil, turnServerUdpAddr) + if err != nil { + return fmt.Errorf("dial TURN (udp): %w", err) + } + defer conn.Close() + turnConn = &connectedUDPConn{conn} + } else { + var d net.Dialer + conn, err := d.DialContext(ctx1, "tcp", turnServerAddr) + if err != nil { + return fmt.Errorf("dial TURN (tcp): %w", err) + } + defer conn.Close() + turnConn = turn.NewSTUNConn(conn) + } + + // 3. Allocate TURN relay + var addrFamily turn.RequestedAddressFamily + if peer.IP.To4() != nil { + addrFamily = turn.RequestedAddressFamilyIPv4 + } else { + addrFamily = turn.RequestedAddressFamilyIPv6 + } + cfg := &turn.ClientConfig{ + STUNServerAddr: turnServerAddr, + TURNServerAddr: turnServerAddr, + Conn: turnConn, + Username: user, + Password: pass, + RequestedAddressFamily: addrFamily, + LoggerFactory: logging.NewDefaultLoggerFactory(), + } + turnClient, err := turn.NewClient(cfg) + if err != nil { + return fmt.Errorf("create TURN client: %w", err) + } + defer turnClient.Close() + if err = turnClient.Listen(); err != nil { + return fmt.Errorf("TURN listen: %w", err) + } + relayConn, err := turnClient.Allocate() + if err != nil { + return fmt.Errorf("TURN allocate: %w", err) + } + defer relayConn.Close() + log.Printf("relayed-address=%s", relayConn.LocalAddr().String()) + + // 4. Establish DTLS over TURN relay + certificate, err := selfsign.GenerateSelfSigned() + if err != nil { + return fmt.Errorf("generate cert: %w", err) + } + + // Create a connected PacketConn for DTLS: relay writes go to peer + dtlsPC := &relayPacketConn{relay: relayConn, peer: peer} + + dtlsConfig := &dtls.Config{ + Certificates: []tls.Certificate{certificate}, + InsecureSkipVerify: true, + ExtendedMasterSecret: dtls.RequireExtendedMasterSecret, + CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, + ConnectionIDGenerator: dtls.OnlySendCIDGenerator(), + } + + dtlsConn, err := dtls.Client(dtlsPC, peer, dtlsConfig) + if err != nil { + return fmt.Errorf("DTLS client create: %w", err) + } + + ctx2, cancel2 := context.WithTimeout(ctx, 30*time.Second) + defer cancel2() + if err = dtlsConn.HandshakeContext(ctx2); err != nil { + dtlsConn.Close() + return fmt.Errorf("DTLS handshake: %w", err) + } + defer dtlsConn.Close() + log.Printf("DTLS connection established") + + // 5. Create KCP session over DTLS + kcpSess, err := tcputil.NewKCPOverDTLS(dtlsConn, false) + if err != nil { + return fmt.Errorf("KCP session: %w", err) + } + defer kcpSess.Close() + log.Printf("KCP session established") + + // 6. Create smux client session over KCP + smuxSess, err := smux.Client(kcpSess, tcputil.DefaultSmuxConfig()) + if err != nil { + return fmt.Errorf("smux client: %w", err) + } + defer smuxSess.Close() + log.Printf("smux session established") + + // 7. Listen for TCP connections and forward through smux + listener, err := net.Listen("tcp", listenAddr) + if err != nil { + return fmt.Errorf("TCP listen: %w", err) + } + context.AfterFunc(ctx, func() { listener.Close() }) + log.Printf("TCP mode: listening on %s", listenAddr) + + var wg sync.WaitGroup + for { + tcpConn, err := listener.Accept() + if err != nil { + select { + case <-ctx.Done(): + wg.Wait() + return nil + default: + } + if smuxSess.IsClosed() { + wg.Wait() + return fmt.Errorf("smux session closed") + } + log.Printf("TCP accept error: %s", err) + continue + } + + wg.Add(1) + go func(tc net.Conn) { + defer wg.Done() + defer tc.Close() + + stream, err := smuxSess.OpenStream() + if err != nil { + log.Printf("smux open stream error: %s", err) + return + } + defer stream.Close() + + pipe(ctx, tc, stream) + }(tcpConn) + } +} + +// relayPacketConn wraps a TURN relay PacketConn to direct all writes to the peer. +type relayPacketConn struct { + relay net.PacketConn + peer net.Addr +} + +func (r *relayPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + return r.relay.ReadFrom(b) +} + +func (r *relayPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) { + return r.relay.WriteTo(b, r.peer) +} + +func (r *relayPacketConn) Close() error { return r.relay.Close() } +func (r *relayPacketConn) LocalAddr() net.Addr { return r.relay.LocalAddr() } +func (r *relayPacketConn) SetDeadline(t time.Time) error { return r.relay.SetDeadline(t) } +func (r *relayPacketConn) SetReadDeadline(t time.Time) error { return r.relay.SetReadDeadline(t) } +func (r *relayPacketConn) SetWriteDeadline(t time.Time) error { return r.relay.SetWriteDeadline(t) } + +// pipe copies data bidirectionally between two connections. +func pipe(ctx context.Context, c1, c2 net.Conn) { + ctx2, cancel := context.WithCancel(ctx) + context.AfterFunc(ctx2, func() { + c1.SetDeadline(time.Now()) + c2.SetDeadline(time.Now()) + }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + defer cancel() + io.Copy(c1, c2) + }() + go func() { + defer wg.Done() + defer cancel() + io.Copy(c2, c1) + }() + wg.Wait() + c1.SetDeadline(time.Time{}) + c2.SetDeadline(time.Time{}) +} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index e67abae..c55b63e 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -3,4 +3,9 @@ set -e CONNECT="${CONNECT_ADDR:?CONNECT_ADDR is required}" -exec ./vk-turn-proxy -listen 0.0.0.0:56000 -connect "$CONNECT" +TCP_FLAG="" +if [ "${TCP_MODE}" = "true" ]; then + TCP_FLAG="-tcp" +fi + +exec ./vk-turn-proxy -listen 0.0.0.0:56000 -connect "$CONNECT" $TCP_FLAG diff --git a/go.mod b/go.mod index db0b818..9881e59 100644 --- a/go.mod +++ b/go.mod @@ -10,14 +10,23 @@ require ( github.com/pion/dtls/v3 v3.0.10 github.com/pion/logging v0.2.4 github.com/pion/turn/v5 v5.0.2 + github.com/xtaci/kcp-go/v5 v5.6.18 + github.com/xtaci/smux v1.5.34 ) require ( + github.com/google/go-cmp v0.7.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/reedsolomon v1.12.4 // indirect github.com/miekg/dns v1.1.69 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/stun/v3 v3.1.1 // indirect github.com/pion/transport/v4 v4.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/templexxx/cpu v0.1.1 // indirect + github.com/templexxx/xorsimd v0.4.3 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/mod v0.30.0 // indirect @@ -25,4 +34,5 @@ require ( golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/tools v0.39.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect ) diff --git a/go.sum b/go.sum index 71673ee..434248e 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,44 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45 h1:0b2i5TvZm8FVcuHP1288k+DEu1XM26DtRjcidOxpGXs= github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45/go.mod h1:NU7MdmhQD8Ounc0760w90fL6nxI2lxjlnIaN6qWzNIU= github.com/cbeuw/connutil v1.0.1 h1:LWuNYjwm7JEDYG/ISAO1TfU4G+q2dA5NhR97eq2roCA= github.com/cbeuw/connutil v1.0.1/go.mod h1:lKofNtrW7Atmosgp1eNnTt2j2NjA2IkifapgLVI1QtA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 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/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA= +github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU= github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg= @@ -26,33 +53,93 @@ github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 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/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI= +github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU= +github.com/templexxx/xorsimd v0.4.3/go.mod h1:oZQcD6RFDisW2Am58dSAGwwL6rHjbzrlu25VDqfWkQg= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xtaci/kcp-go/v5 v5.6.18 h1:7oV4mc272pcnn39/13BB11Bx7hJM4ogMIEokJYVWn4g= +github.com/xtaci/kcp-go/v5 v5.6.18/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= +github.com/xtaci/smux v1.5.34 h1:OUA9JaDFHJDT8ZT3ebwLWPAgEfE6sWo2LaTy3anXqwg= +github.com/xtaci/smux v1.5.34/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/server/main.go b/server/main.go index 819f9b5..aac6a41 100644 --- a/server/main.go +++ b/server/main.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "flag" "fmt" + "io" "log" "net" "os" @@ -13,13 +14,16 @@ import ( "syscall" "time" + "github.com/cacggghp/vk-turn-proxy/tcputil" "github.com/pion/dtls/v3" "github.com/pion/dtls/v3/pkg/crypto/selfsign" + "github.com/xtaci/smux" ) func main() { listen := flag.String("listen", "0.0.0.0:56000", "listen on ip:port") connect := flag.String("connect", "", "connect to ip:port") + tcpMode := flag.Bool("tcp", false, "TCP mode: forward TCP connections (for VLESS) instead of UDP packets") flag.Parse() ctx, cancel := context.WithCancel(context.Background()) @@ -47,10 +51,6 @@ func main() { panic(err) } - // - // Everything below is the pion-DTLS API! Thanks for using it ❤️. - // - // Prepare the configuration of the DTLS connection config := &dtls.Config{ Certificates: []tls.Certificate{certificate}, @@ -59,7 +59,7 @@ func main() { ConnectionIDGenerator: dtls.RandomCIDGenerator(8), } - // Connect to a DTLS server + // Listen for DTLS connections listener, err := dtls.Listen("udp", addr, config) if err != nil { panic(err) @@ -94,11 +94,7 @@ func main() { log.Printf("failed to close incoming connection: %s", closeErr) } }() - var err error = nil log.Printf("Connection from %s\n", conn.RemoteAddr()) - // `conn` is of type `net.Conn` but may be casted to `dtls.Conn` - // using `dtlsConn := conn.(*dtls.Conn)` in order to to expose - // functions like `ConnectionState` etc. // Perform the handshake with a 30-second timeout ctx1, cancel1 := context.WithTimeout(ctx, 30*time.Second) @@ -109,7 +105,7 @@ func main() { return } log.Println("Start handshake") - if err = dtlsConn.HandshakeContext(ctx1); err != nil { + if err := dtlsConn.HandshakeContext(ctx1); err != nil { log.Println(err) cancel1() return @@ -117,93 +113,180 @@ func main() { cancel1() log.Println("Handshake done") - serverConn, err := net.Dial("udp", *connect) + if *tcpMode { + handleTCPConnection(ctx, dtlsConn, *connect) + } else { + handleUDPConnection(ctx, conn, *connect) + } + log.Printf("Connection closed: %s\n", conn.RemoteAddr()) + }(conn) + } +} + +// handleUDPConnection forwards DTLS packets to a UDP backend (WireGuard). +func handleUDPConnection(ctx context.Context, conn net.Conn, connectAddr string) { + serverConn, err := net.Dial("udp", connectAddr) + if err != nil { + log.Println(err) + return + } + defer func() { + if err = serverConn.Close(); err != nil { + log.Printf("failed to close outgoing connection: %s", err) + } + }() + + var wg sync.WaitGroup + wg.Add(2) + ctx2, cancel2 := context.WithCancel(ctx) + context.AfterFunc(ctx2, func() { + if err := conn.SetDeadline(time.Now()); err != nil { + log.Printf("failed to set incoming deadline: %s", err) + } + if err := serverConn.SetDeadline(time.Now()); err != nil { + log.Printf("failed to set outgoing deadline: %s", err) + } + }) + go func() { + defer wg.Done() + defer cancel2() + buf := make([]byte, 1600) + for { + select { + case <-ctx2.Done(): + return + default: + } + if err1 := conn.SetReadDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + log.Printf("Failed: %s", err1) + return + } + n, err1 := conn.Read(buf) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + + if err1 := serverConn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + log.Printf("Failed: %s", err1) + return + } + _, err1 = serverConn.Write(buf[:n]) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + } + }() + go func() { + defer wg.Done() + defer cancel2() + buf := make([]byte, 1600) + for { + select { + case <-ctx2.Done(): + return + default: + } + if err1 := serverConn.SetReadDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + log.Printf("Failed: %s", err1) + return + } + n, err1 := serverConn.Read(buf) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + + if err1 := conn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + log.Printf("Failed: %s", err1) + return + } + _, err1 = conn.Write(buf[:n]) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + } + }() + wg.Wait() +} + +// handleTCPConnection creates a KCP+smux session over DTLS and forwards +// each smux stream as a TCP connection to the backend (Xray/VLESS). +func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr string) { + // 1. Create KCP session over DTLS + kcpSess, err := tcputil.NewKCPOverDTLS(dtlsConn, true) + if err != nil { + log.Printf("KCP session error: %s", err) + return + } + defer kcpSess.Close() + log.Printf("KCP session established (server)") + + // 2. Create smux server session over KCP + smuxSess, err := smux.Server(kcpSess, tcputil.DefaultSmuxConfig()) + if err != nil { + log.Printf("smux server error: %s", err) + return + } + defer smuxSess.Close() + log.Printf("smux session established (server)") + + // 3. Accept smux streams and forward to backend via TCP + var wg sync.WaitGroup + for { + stream, err := smuxSess.AcceptStream() + if err != nil { + select { + case <-ctx.Done(): + default: + log.Printf("smux accept error: %s", err) + } + break + } + + wg.Add(1) + go func(s *smux.Stream) { + defer wg.Done() + defer s.Close() + + // Connect to backend (Xray/VLESS) + backendConn, err := net.DialTimeout("tcp", connectAddr, 10*time.Second) if err != nil { - log.Println(err) + log.Printf("backend dial error: %s", err) return } - defer func() { - if err = serverConn.Close(); err != nil { - log.Printf("failed to close outgoing connection: %s", err) - return - } - }() + defer backendConn.Close() - var wg sync.WaitGroup - wg.Add(2) - ctx2, cancel2 := context.WithCancel(ctx) - context.AfterFunc(ctx2, func() { - if err := conn.SetDeadline(time.Now()); err != nil { - log.Printf("failed to set incoming deadline: %s", err) - } - if err := serverConn.SetDeadline(time.Now()); err != nil { - log.Printf("failed to set outgoing deadline: %s", err) - } - }) - go func() { - defer wg.Done() - defer cancel2() - buf := make([]byte, 1600) - for { - select { - case <-ctx2.Done(): - return - default: - } - if err1 := conn.SetReadDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { - log.Printf("Failed: %s", err1) - return - } - n, err1 := conn.Read(buf) - if err1 != nil { - log.Printf("Failed: %s", err1) - return - } - - if err1 := serverConn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { - log.Printf("Failed: %s", err1) - return - } - _, err1 = serverConn.Write(buf[:n]) - if err1 != nil { - log.Printf("Failed: %s", err1) - return - } - } - }() - go func() { - defer wg.Done() - defer cancel2() - buf := make([]byte, 1600) - for { - select { - case <-ctx2.Done(): - return - default: - } - if err1 := serverConn.SetReadDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { - log.Printf("Failed: %s", err1) - return - } - n, err1 := serverConn.Read(buf) - if err1 != nil { - log.Printf("Failed: %s", err1) - return - } - - if err1 := conn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { - log.Printf("Failed: %s", err1) - return - } - _, err1 = conn.Write(buf[:n]) - if err1 != nil { - log.Printf("Failed: %s", err1) - return - } - } - }() - wg.Wait() - log.Printf("Connection closed: %s\n", conn.RemoteAddr()) - }(conn) + // Bidirectional copy + pipeConn(ctx, s, backendConn) + }(stream) } + wg.Wait() +} + +// pipeConn copies data bidirectionally between two connections. +func pipeConn(ctx context.Context, c1, c2 net.Conn) { + ctx2, cancel := context.WithCancel(ctx) + context.AfterFunc(ctx2, func() { + c1.SetDeadline(time.Now()) + c2.SetDeadline(time.Now()) + }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + defer cancel() + io.Copy(c1, c2) + }() + go func() { + defer wg.Done() + defer cancel() + io.Copy(c2, c1) + }() + wg.Wait() + c1.SetDeadline(time.Time{}) + c2.SetDeadline(time.Time{}) } diff --git a/tcputil/tcputil.go b/tcputil/tcputil.go new file mode 100644 index 0000000..260cce3 --- /dev/null +++ b/tcputil/tcputil.go @@ -0,0 +1,99 @@ +package tcputil + +import ( + "net" + "time" + + "github.com/xtaci/kcp-go/v5" + "github.com/xtaci/smux" +) + +// DtlsPacketConn wraps a net.Conn (DTLS) as a net.PacketConn for KCP. +// Each DTLS Read/Write preserves message boundaries (datagram semantics). +type DtlsPacketConn struct { + conn net.Conn +} + +func NewDtlsPacketConn(conn net.Conn) *DtlsPacketConn { + return &DtlsPacketConn{conn: conn} +} + +func (d *DtlsPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, err := d.conn.Read(b) + return n, d.conn.RemoteAddr(), err +} + +func (d *DtlsPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) { + return d.conn.Write(b) +} + +func (d *DtlsPacketConn) Close() error { + return d.conn.Close() +} + +func (d *DtlsPacketConn) LocalAddr() net.Addr { + return d.conn.LocalAddr() +} + +func (d *DtlsPacketConn) SetDeadline(t time.Time) error { + return d.conn.SetDeadline(t) +} + +func (d *DtlsPacketConn) SetReadDeadline(t time.Time) error { + return d.conn.SetReadDeadline(t) +} + +func (d *DtlsPacketConn) SetWriteDeadline(t time.Time) error { + return d.conn.SetWriteDeadline(t) +} + +// NewKCPOverDTLS creates a KCP session over a DTLS connection. +// isServer: true for server-side (listener), false for client-side (dialer). +func NewKCPOverDTLS(dtlsConn net.Conn, isServer bool) (*kcp.UDPSession, error) { + pc := NewDtlsPacketConn(dtlsConn) + + block, _ := kcp.NewNoneBlockCrypt(nil) // DTLS already encrypts + + var sess *kcp.UDPSession + var err error + + if isServer { + // Server: listen on the PacketConn and accept one session + listener, err := kcp.ServeConn(block, 0, 0, pc) + if err != nil { + return nil, err + } + listener.SetDeadline(time.Now().Add(30 * time.Second)) + sess, err = listener.AcceptKCP() + if err != nil { + return nil, err + } + } else { + // Client: dial through the PacketConn + sess, err = kcp.NewConn2(dtlsConn.RemoteAddr(), block, 0, 0, pc) + if err != nil { + return nil, err + } + } + + // Tune KCP for TURN tunnel: + // - NoDelay mode for lower latency + // - Window sizes suitable for ~5Mbit/s + sess.SetNoDelay(1, 20, 2, 1) // nodelay, interval(ms), resend, nc + sess.SetWindowSize(256, 256) + sess.SetMtu(1200) // conservative MTU to fit inside DTLS+TURN + sess.SetACKNoDelay(true) + sess.SetStreamMode(true) + + return sess, nil +} + +// DefaultSmuxConfig returns smux config tuned for TURN tunnel. +func DefaultSmuxConfig() *smux.Config { + cfg := smux.DefaultConfig() + cfg.MaxReceiveBuffer = 4 * 1024 * 1024 + cfg.MaxStreamBuffer = 1 * 1024 * 1024 + cfg.KeepAliveInterval = 10 * time.Second + cfg.KeepAliveTimeout = 30 * time.Second + return cfg +}