From b798ec57445a6c3d60949df9241e9877e849da1a Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Tue, 31 Mar 2026 02:15:25 +0700 Subject: [PATCH 01/14] feat: TCP mode for VLESS support via KCP+smux over TURN tunnel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add -tcp flag to client and server for forwarding TCP connections (VLESS, etc.) through the TURN tunnel instead of UDP packets (WireGuard). Stack: TCP → smux (mux) → KCP (reliability) → DTLS (encryption) → TURN New dependencies: xtaci/kcp-go/v5, xtaci/smux --- README.md | 115 ++++++++++++++++++ client/main.go | 260 +++++++++++++++++++++++++++++++++++++++-- docker-entrypoint.sh | 7 +- go.mod | 10 ++ go.sum | 91 ++++++++++++++- server/main.go | 271 ++++++++++++++++++++++++++++--------------- tcputil/tcputil.go | 99 ++++++++++++++++ 7 files changed, 748 insertions(+), 105 deletions(-) create mode 100644 tcputil/tcputil.go 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 +} From 8acc5535f7aae2844cd39056918cf4991a10bde0 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Fri, 3 Apr 2026 14:33:18 +0700 Subject: [PATCH 02/14] feat: round-robin multi-session TCP mode for increased throughput Distribute incoming TCP connections across N parallel TURN+DTLS+KCP+smux sessions in round-robin fashion, aggregating bandwidth of multiple relays. Add sessionPool with thread-safe add/remove/pick operations. Each session is maintained by its own goroutine with auto-reconnect. The -n flag now controls session count in TCP mode (default 16 for VK). Refactor: extract createSmuxSession from runTCPSession for reuse. --- client/main.go | 266 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 188 insertions(+), 78 deletions(-) diff --git a/client/main.go b/client/main.go index 58e347b..0ebc343 100644 --- a/client/main.go +++ b/client/main.go @@ -884,7 +884,7 @@ func main() { //nolint:cyclop } if *tcpMode { - runTCPMode(ctx, params, peer, *listen) + runTCPMode(ctx, params, peer, *listen, *n) return } @@ -946,10 +946,126 @@ 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) { +// sessionPool manages a pool of smux sessions for round-robin TCP distribution. +type sessionPool struct { + mu sync.RWMutex + sessions []*smux.Session + counter atomic.Uint64 +} + +func (p *sessionPool) add(s *smux.Session) { + p.mu.Lock() + p.sessions = append(p.sessions, s) + p.mu.Unlock() +} + +func (p *sessionPool) remove(s *smux.Session) { + p.mu.Lock() + for i, sess := range p.sessions { + if sess == s { + p.sessions = append(p.sessions[:i], p.sessions[i+1:]...) + break + } + } + p.mu.Unlock() +} + +func (p *sessionPool) pick() *smux.Session { + p.mu.RLock() + defer p.mu.RUnlock() + n := len(p.sessions) + if n == 0 { + return nil + } + idx := p.counter.Add(1) % uint64(n) + return p.sessions[idx] +} + +func (p *sessionPool) count() int { + p.mu.RLock() + defer p.mu.RUnlock() + return len(p.sessions) +} + +// runTCPMode implements TCP forwarding with round-robin across N TURN sessions. +func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAddr string, numSessions int) { + pool := &sessionPool{} + + // Start N session maintainers with staggered startup + var wgMaint sync.WaitGroup + for i := 0; i < numSessions; i++ { + wgMaint.Add(1) + go func(id int) { + defer wgMaint.Done() + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(id) * 300 * time.Millisecond): + } + maintainTCPSession(ctx, tp, peer, id, pool) + }(i) + } + + // Wait for at least one session + log.Printf("TCP mode: waiting for sessions to connect (total: %d)...", numSessions) + for { + select { + case <-ctx.Done(): + wgMaint.Wait() + return + case <-time.After(100 * time.Millisecond): + } + if pool.count() > 0 { + break + } + } + + listener, err := net.Listen("tcp", listenAddr) + if err != nil { + log.Panicf("TCP listen: %s", err) + } + context.AfterFunc(ctx, func() { listener.Close() }) + log.Printf("TCP mode: listening on %s (round-robin across %d sessions)", listenAddr, numSessions) + + var wgConn sync.WaitGroup + for { + tcpConn, err := listener.Accept() + if err != nil { + select { + case <-ctx.Done(): + wgConn.Wait() + wgMaint.Wait() + return + default: + } + log.Printf("TCP accept error: %s", err) + continue + } + + sess := pool.pick() + if sess == nil || sess.IsClosed() { + log.Printf("No active sessions, rejecting connection") + tcpConn.Close() + continue + } + + wgConn.Add(1) + go func(tc net.Conn, s *smux.Session) { + defer wgConn.Done() + defer tc.Close() + stream, err := s.OpenStream() + if err != nil { + log.Printf("smux open stream error: %s", err) + return + } + defer stream.Close() + pipe(ctx, tc, stream) + }(tcpConn, sess) + } +} + +// maintainTCPSession keeps one TURN+DTLS+KCP+smux session alive, reconnecting on failure. +func maintainTCPSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, id int, pool *sessionPool) { for { select { case <-ctx.Done(): @@ -957,11 +1073,34 @@ func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAd default: } - err := runTCPSession(ctx, tp, peer, listenAddr) + smuxSess, cleanup, err := createSmuxSession(ctx, tp, peer) if err != nil { - log.Printf("TCP session error: %s, reconnecting...", err) + log.Printf("[session %d] setup error: %s, retrying...", id, err) + select { + case <-ctx.Done(): + return + case <-time.After(3 * time.Second): + } + continue } + pool.add(smuxSess) + log.Printf("[session %d] connected (active: %d)", id, pool.count()) + + for !smuxSess.IsClosed() { + select { + case <-ctx.Done(): + pool.remove(smuxSess) + cleanup() + return + case <-time.After(1 * time.Second): + } + } + + pool.remove(smuxSess) + cleanup() + log.Printf("[session %d] disconnected (active: %d), reconnecting...", id, pool.count()) + select { case <-ctx.Done(): return @@ -970,15 +1109,24 @@ func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAd } } -func runTCPSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAddr string) error { +// createSmuxSession establishes a full TURN+DTLS+KCP+smux pipeline and returns +// the smux session along with a cleanup function to tear down all layers. +func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr) (*smux.Session, func(), error) { + var cleanupFns []func() + cleanup := func() { + for i := len(cleanupFns) - 1; i >= 0; i-- { + cleanupFns[i]() + } + } + // 1. Get TURN credentials - user, pass, url, err := tp.getCreds(tp.link) + user, pass, rawURL, err := tp.getCreds(tp.link) if err != nil { - return fmt.Errorf("get TURN creds: %w", err) + return nil, nil, fmt.Errorf("get TURN creds: %w", err) } - urlhost, urlport, err := net.SplitHostPort(url) + urlhost, urlport, err := net.SplitHostPort(rawURL) if err != nil { - return fmt.Errorf("parse TURN addr: %w", err) + return nil, nil, fmt.Errorf("parse TURN addr: %w", err) } if tp.host != "" { urlhost = tp.host @@ -989,10 +1137,9 @@ func runTCPSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, liste turnServerAddr := net.JoinHostPort(urlhost, urlport) turnServerUdpAddr, err := net.ResolveUDPAddr("udp", turnServerAddr) if err != nil { - return fmt.Errorf("resolve TURN addr: %w", err) + return nil, nil, fmt.Errorf("resolve TURN addr: %w", err) } turnServerAddr = turnServerUdpAddr.String() - fmt.Println(turnServerUdpAddr.IP) // 2. Connect to TURN server var turnConn net.PacketConn @@ -1001,21 +1148,21 @@ func runTCPSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, liste if tp.udp { conn, err := net.DialUDP("udp", nil, turnServerUdpAddr) if err != nil { - return fmt.Errorf("dial TURN (udp): %w", err) + return nil, nil, fmt.Errorf("dial TURN (udp): %w", err) } - defer conn.Close() + cleanupFns = append(cleanupFns, func() { 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) + return nil, nil, fmt.Errorf("dial TURN (tcp): %w", err) } - defer conn.Close() + cleanupFns = append(cleanupFns, func() { conn.Close() }) turnConn = turn.NewSTUNConn(conn) } - // 3. Allocate TURN relay + // 3. Create TURN client and allocate relay var addrFamily turn.RequestedAddressFamily if peer.IP.To4() != nil { addrFamily = turn.RequestedAddressFamilyIPv4 @@ -1033,28 +1180,29 @@ func runTCPSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, liste } turnClient, err := turn.NewClient(cfg) if err != nil { - return fmt.Errorf("create TURN client: %w", err) + cleanup() + return nil, nil, fmt.Errorf("create TURN client: %w", err) } - defer turnClient.Close() + cleanupFns = append(cleanupFns, func() { turnClient.Close() }) if err = turnClient.Listen(); err != nil { - return fmt.Errorf("TURN listen: %w", err) + cleanup() + return nil, nil, fmt.Errorf("TURN listen: %w", err) } relayConn, err := turnClient.Allocate() if err != nil { - return fmt.Errorf("TURN allocate: %w", err) + cleanup() + return nil, nil, fmt.Errorf("TURN allocate: %w", err) } - defer relayConn.Close() + cleanupFns = append(cleanupFns, func() { 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) + cleanup() + return nil, nil, 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, @@ -1062,78 +1210,40 @@ func runTCPSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, liste 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) + cleanup() + return nil, nil, 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) + cleanup() + return nil, nil, fmt.Errorf("DTLS handshake: %w", err) } - defer dtlsConn.Close() + cleanupFns = append(cleanupFns, func() { 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) + cleanup() + return nil, nil, fmt.Errorf("KCP session: %w", err) } - defer kcpSess.Close() + cleanupFns = append(cleanupFns, func() { 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) + cleanup() + return nil, nil, fmt.Errorf("smux client: %w", err) } - defer smuxSess.Close() + cleanupFns = append(cleanupFns, func() { 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) - } + return smuxSess, cleanup, nil } // relayPacketConn wraps a TURN relay PacketConn to direct all writes to the peer. From 4a0b038bdc5097f46e59fdf18013a09338900d4c Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Sat, 4 Apr 2026 15:37:04 +0700 Subject: [PATCH 03/14] feat: optimize VK TURN credential retrieval and reuse session identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлено кэширование success_token для капчи. Установлен размер пула 1 для использования одной учетной записи во всех потоках. Дополнительно устранил warnings от golangcli-lint: Удален rand.Seed Реализована проверка ошибки в endSession Реализована обработка Body.Close() --- client/main.go | 35 ++++++++++++++++++++++++++++++----- go.mod | 2 +- routes.ps1 | 2 +- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/client/main.go b/client/main.go index c3bf1eb..4903aff 100644 --- a/client/main.go +++ b/client/main.go @@ -238,7 +238,11 @@ func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Di if err != nil { return "", 0, err } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + log.Printf("Failed to close response body: %v", closeErr) + } + }() body, err := io.ReadAll(resp.Body) if err != nil { @@ -300,7 +304,11 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer if err != nil { return nil, err } - defer httpResp.Body.Close() + defer func() { + if closeErr := httpResp.Body.Close(); closeErr != nil { + log.Printf("Failed to close response body: %v", closeErr) + } + }() body, err := io.ReadAll(httpResp.Body) if err != nil { @@ -365,13 +373,20 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer time.Sleep(200 * time.Millisecond) // Step 4: endSession - vkReq("captchaNotRobot.endSession", baseParams) + if _, err := vkReq("captchaNotRobot.endSession", baseParams); err != nil { + log.Printf("endSession failed: %v", err) + } return successToken, nil } // endregion automatic captcha solver +var ( + cachedCaptchaTokenMu sync.Mutex + cachedCaptchaToken string +) + func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, error) { profile := getRandomProfile() name := generateName() @@ -446,7 +461,14 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, return "", "", "", fmt.Errorf("missing access_token in response: %v", resp) } + cachedCaptchaTokenMu.Lock() + curSuccessToken := cachedCaptchaToken + cachedCaptchaTokenMu.Unlock() + data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=%s&access_token=%s", link, escapedName, token1) + if curSuccessToken != "" { + data += fmt.Sprintf("&success_token=%s", neturl.QueryEscape(curSuccessToken)) + } url = "https://api.vk.ru/method/calls.getAnonymousToken?v=5.274&client_id=6287487" var token2 string @@ -472,6 +494,10 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, return "", "", "", fmt.Errorf("auto captcha solve error: %w", solveErr) } + cachedCaptchaTokenMu.Lock() + cachedCaptchaToken = successToken + cachedCaptchaTokenMu.Unlock() + if captchaErr.CaptchaAttempt == "0" || captchaErr.CaptchaAttempt == "" { captchaErr.CaptchaAttempt = "1" } @@ -1263,7 +1289,6 @@ func poolCreds(f getCredsFunc, poolSize int) getCredsFunc { } func main() { //nolint:cyclop - rand.Seed(time.Now().UnixNano()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() signalChan := make(chan os.Signal, 1) @@ -1335,7 +1360,7 @@ func main() { //nolint:cyclop port: *port, link: link, udp: *udp, - getCreds: poolCreds(getCreds, *n), + getCreds: poolCreds(getCreds, 1), } if *tcpMode { diff --git a/go.mod b/go.mod index da118ad..92afaa4 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/pion/dtls/v3 v3.0.11 github.com/pion/logging v0.2.4 + github.com/pion/transport/v4 v4.0.1 github.com/pion/turn/v5 v5.0.2 github.com/xtaci/kcp-go/v5 v5.6.18 github.com/xtaci/smux v1.5.34 @@ -22,7 +23,6 @@ require ( 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 diff --git a/routes.ps1 b/routes.ps1 index 2c2464e..18ffc0d 100644 --- a/routes.ps1 +++ b/routes.ps1 @@ -1,4 +1,4 @@ -# Получаем default gateway (IPv4) +# Получаем default gateway (IPv4) $gateway = Get-NetRoute ` -DestinationPrefix "0.0.0.0/0" ` | Sort-Object RouteMetric ` From 7abbd4986f8a93871fb840ae5bdc61d928593048 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Sat, 4 Apr 2026 15:56:21 +0700 Subject: [PATCH 04/14] fix: resolve golangci-lint warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена проверка ошибки listener.SetDeadline в tcputil. Удален вызов устаревшего метода SetStreamMode в KCP. Добавлено явное игнорирование возвращаемых значений SetDeadline и io.Copy . --- client/main.go | 12 ++++++------ server/main.go | 12 ++++++------ tcputil/tcputil.go | 5 +++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/client/main.go b/client/main.go index 4903aff..26fcfbc 100644 --- a/client/main.go +++ b/client/main.go @@ -1760,8 +1760,8 @@ func (r *relayPacketConn) SetWriteDeadline(t time.Time) error { return r.relay.S 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()) + _ = c1.SetDeadline(time.Now()) + _ = c2.SetDeadline(time.Now()) }) var wg sync.WaitGroup @@ -1769,14 +1769,14 @@ func pipe(ctx context.Context, c1, c2 net.Conn) { go func() { defer wg.Done() defer cancel() - io.Copy(c1, c2) + _, _ = io.Copy(c1, c2) }() go func() { defer wg.Done() defer cancel() - io.Copy(c2, c1) + _, _ = io.Copy(c2, c1) }() wg.Wait() - c1.SetDeadline(time.Time{}) - c2.SetDeadline(time.Time{}) + _ = c1.SetDeadline(time.Time{}) + _ = c2.SetDeadline(time.Time{}) } diff --git a/server/main.go b/server/main.go index aac6a41..ad4b368 100644 --- a/server/main.go +++ b/server/main.go @@ -270,8 +270,8 @@ func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr str 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()) + _ = c1.SetDeadline(time.Now()) + _ = c2.SetDeadline(time.Now()) }) var wg sync.WaitGroup @@ -279,14 +279,14 @@ func pipeConn(ctx context.Context, c1, c2 net.Conn) { go func() { defer wg.Done() defer cancel() - io.Copy(c1, c2) + _, _ = io.Copy(c1, c2) }() go func() { defer wg.Done() defer cancel() - io.Copy(c2, c1) + _, _ = io.Copy(c2, c1) }() wg.Wait() - c1.SetDeadline(time.Time{}) - c2.SetDeadline(time.Time{}) + _ = c1.SetDeadline(time.Time{}) + _ = c2.SetDeadline(time.Time{}) } diff --git a/tcputil/tcputil.go b/tcputil/tcputil.go index 260cce3..f36bbdc 100644 --- a/tcputil/tcputil.go +++ b/tcputil/tcputil.go @@ -63,7 +63,9 @@ func NewKCPOverDTLS(dtlsConn net.Conn, isServer bool) (*kcp.UDPSession, error) { if err != nil { return nil, err } - listener.SetDeadline(time.Now().Add(30 * time.Second)) + if err = listener.SetDeadline(time.Now().Add(30 * time.Second)); err != nil { + return nil, err + } sess, err = listener.AcceptKCP() if err != nil { return nil, err @@ -83,7 +85,6 @@ func NewKCPOverDTLS(dtlsConn net.Conn, isServer bool) (*kcp.UDPSession, error) { sess.SetWindowSize(256, 256) sess.SetMtu(1200) // conservative MTU to fit inside DTLS+TURN sess.SetACKNoDelay(true) - sess.SetStreamMode(true) return sess, nil } From 03f9d424c5e9951cb8faaa0722b7eb297797b53c Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Sat, 4 Apr 2026 16:44:33 +0700 Subject: [PATCH 05/14] fix: resolve golangci-lint warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Обновлен .golangci.yml В ci.yml установлены версии Go 'stable' и golangci-lint 'latest' --- .github/workflows/ci.yml | 6 +- .golangci.yml | 51 ++++++++++ client/main.go | 201 ++++++++++++++++++++++++++------------- server/main.go | 36 ++++--- tcputil/tcputil.go | 9 +- 5 files changed, 217 insertions(+), 86 deletions(-) create mode 100644 .golangci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e26950..c913016 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: 'stable' - name: Cache Go modules uses: actions/cache@v5 @@ -47,7 +47,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.8.0 + version: latest args: --timeout=5m ./... env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -87,7 +87,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: 'stable' # Install Android NDK for android rows and expose ndk-path output - name: Setup Android NDK diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..68d98b3 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,51 @@ +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - unused + - revive + - misspell + - bodyclose + +linters-settings: + errcheck: + check-blank: true + check-type-assertions: true + + govet: + enable-all: true + disable: + - fieldalignment + + revive: + rules: + - name: blank-imports + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + +run: + timeout: 5m + tests: true + +issues: + exclude-use-default: true + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/client/main.go b/client/main.go index 26fcfbc..080b0b6 100644 --- a/client/main.go +++ b/client/main.go @@ -148,18 +148,33 @@ type vkCaptchaError struct { ErrorCode int ErrorMsg string CaptchaSid string - RedirectUri string + RedirectURI string SessionToken string CaptchaTs string CaptchaAttempt string } func parseVkCaptchaError(errData map[string]interface{}) *vkCaptchaError { - codeFloat, _ := errData["error_code"].(float64) - redirectUri, _ := errData["redirect_uri"].(string) - errorMsg, _ := errData["error_msg"].(string) + var codeFloat float64 + if val, ok := errData["error_code"].(float64); ok { + codeFloat = val + } + + var redirectURI string + if val, ok := errData["redirect_uri"].(string); ok { + redirectURI = val + } + + var errorMsg string + if val, ok := errData["error_msg"].(string); ok { + errorMsg = val + } + + var captchaSid string + if val, ok := errData["captcha_sid"].(string); ok { + captchaSid = val + } - captchaSid, _ := errData["captcha_sid"].(string) if captchaSid == "" { if sidNum, ok := errData["captcha_sid"].(float64); ok { captchaSid = fmt.Sprintf("%.0f", sidNum) @@ -167,8 +182,8 @@ func parseVkCaptchaError(errData map[string]interface{}) *vkCaptchaError { } var sessionToken string - if redirectUri != "" { - if parsed, err := neturl.Parse(redirectUri); err == nil { + if redirectURI != "" { + if parsed, err := neturl.Parse(redirectURI); err == nil { sessionToken = parsed.Query().Get("session_token") } } @@ -191,7 +206,7 @@ func parseVkCaptchaError(errData map[string]interface{}) *vkCaptchaError { ErrorCode: int(codeFloat), ErrorMsg: errorMsg, CaptchaSid: captchaSid, - RedirectUri: redirectUri, + RedirectURI: redirectURI, SessionToken: sessionToken, CaptchaTs: captchaTs, CaptchaAttempt: captchaAttempt, @@ -204,7 +219,7 @@ func solveVkCaptcha(ctx context.Context, captchaErr *vkCaptchaError, dialer *dns return "", fmt.Errorf("no session_token in redirect_uri") } - powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectUri, dialer) + powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectURI, dialer) if err != nil { return "", fmt.Errorf("failed to fetch PoW input: %w", err) } @@ -220,8 +235,8 @@ func solveVkCaptcha(ctx context.Context, captchaErr *vkCaptchaError, dialer *dns return successToken, nil } -func fetchPowInput(ctx context.Context, redirectUri string, dialer *dnsdialer.Dialer) (string, int, error) { - req, err := http.NewRequestWithContext(ctx, "GET", redirectUri, nil) +func fetchPowInput(ctx context.Context, redirectURI string, dialer *dnsdialer.Dialer) (string, int, error) { + req, err := http.NewRequestWithContext(ctx, "GET", redirectURI, nil) if err != nil { return "", 0, err } @@ -361,8 +376,8 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, dialer if !ok { return "", fmt.Errorf("invalid check response: %v", checkResp) } - status, _ := respObj["status"].(string) - if status != "OK" { + status, ok := respObj["status"].(string) + if !ok || status != "OK" { return "", fmt.Errorf("check status: %s", status) } successToken, ok := respObj["success_token"].(string) @@ -481,8 +496,8 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, // Check for captcha error if errObj, hasErr := resp["error"].(map[string]interface{}); hasErr { - errCode, _ := errObj["error_code"].(float64) - if errCode == 14 { + errCode, ok2 := errObj["error_code"].(float64) + if ok2 && errCode == 14 { if attempt == maxCaptchaAttempts { return "", "", "", fmt.Errorf("captcha failed after %d attempts", maxCaptchaAttempts) } @@ -512,12 +527,12 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, return "", "", "", fmt.Errorf("VK API error: %v", errObj) } - respMap, ok := resp["response"].(map[string]interface{}) - if !ok { + respMap, okLoop := resp["response"].(map[string]interface{}) + if !okLoop { return "", "", "", fmt.Errorf("unexpected getAnonymousToken response: %v", resp) } - token2, ok = respMap["token"].(string) - if !ok { + token2, okLoop = respMap["token"].(string) + if !okLoop { return "", "", "", fmt.Errorf("missing token in response: %v", resp) } break @@ -531,7 +546,10 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, return "", "", "", fmt.Errorf("request error:%s", err) } - token3 := resp["session_key"].(string) + token3, ok := resp["session_key"].(string) + if !ok { + return "", "", "", fmt.Errorf("missing session_key in response: %v", resp) + } data = fmt.Sprintf("joinLink=%s&isVideo=false&protocolVersion=5&anonymToken=%s&method=vchat.joinConversationByLink&format=JSON&application_key=CGMMEJLGDIHBABABA&session_key=%s", link, token2, token3) url = "https://calls.okcdn.ru/fb.do" @@ -541,9 +559,30 @@ func getVkCreds(link string, dialer *dnsdialer.Dialer) (string, string, string, return "", "", "", fmt.Errorf("request error:%s", err) } - user := resp["turn_server"].(map[string]interface{})["username"].(string) - pass := resp["turn_server"].(map[string]interface{})["credential"].(string) - turn := resp["turn_server"].(map[string]interface{})["urls"].([]interface{})[0].(string) + turnServer, ok2 := resp["turn_server"].(map[string]interface{}) + if !ok2 { + return "", "", "", fmt.Errorf("missing turn_server in response: %v", resp) + } + + user, ok2 := turnServer["username"].(string) + if !ok2 { + return "", "", "", fmt.Errorf("missing username in turn_server: %v", turnServer) + } + + pass, ok2 := turnServer["credential"].(string) + if !ok2 { + return "", "", "", fmt.Errorf("missing credential in turn_server: %v", turnServer) + } + + urls, ok2 := turnServer["urls"].([]interface{}) + if !ok2 || len(urls) == 0 { + return "", "", "", fmt.Errorf("missing or empty urls in turn_server: %v", turnServer) + } + + turn, ok2 := urls[0].(string) + if !ok2 { + return "", "", "", fmt.Errorf("first url is not a string: %v", urls[0]) + } clean := strings.Split(turn, "?")[0] address := strings.TrimPrefix(strings.TrimPrefix(clean, "turn:"), "turns:") @@ -655,7 +694,7 @@ func getYandexCreds(link string) (string, string, string, error) { } type WSSAck struct { - Uid string `json:"uid"` + UID string `json:"uid"` Ack struct { Status struct { Code string `json:"code"` @@ -664,8 +703,8 @@ func getYandexCreds(link string) (string, string, string, error) { } type WSSData struct { - ParticipantId string - RoomId string + ParticipantID string + RoomID string Credentials string Wss string } @@ -701,8 +740,11 @@ func getYandexCreds(link string) (string, string, string, error) { } }() if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return "", "", "", fmt.Errorf("GetConference: status=%s body=%s", resp.Status, string(body)) + readBody, err2 := io.ReadAll(resp.Body) + if err2 != nil { + return "", "", "", fmt.Errorf("GetConference: status=%s (failed to read body: %v)", resp.Status, err2) + } + return "", "", "", fmt.Errorf("GetConference: status=%s body=%s", resp.Status, string(readBody)) } var result ConferenceResponse @@ -710,8 +752,8 @@ func getYandexCreds(link string) (string, string, string, error) { return "", "", "", fmt.Errorf("decode conf: %v", err) } data := WSSData{ - ParticipantId: result.PeerID, - RoomId: result.RoomID, + ParticipantID: result.PeerID, + RoomID: result.RoomID, Credentials: result.Credentials, Wss: result.ClientConfiguration.MediaServerURL, } @@ -723,10 +765,17 @@ func getYandexCreds(link string) (string, string, string, error) { defer cancel() dialer := websocket.Dialer{} - conn, _, err := dialer.DialContext(ctx, data.Wss, h) + var conn *websocket.Conn + conn, resp, err = dialer.DialContext(ctx, data.Wss, h) if err != nil { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } return "", "", "", fmt.Errorf("ws dial: %w", err) } + if resp != nil && resp.Body != nil { + defer func() { _ = resp.Body.Close() }() + } defer func() { if closeErr := conn.Close(); closeErr != nil { log.Printf("close websocket: %s", closeErr) @@ -752,8 +801,8 @@ func getYandexCreds(link string) (string, string, string, error) { SendVideo: false, SendSharing: false, - ParticipantID: data.ParticipantId, - RoomID: data.RoomId, + ParticipantID: data.ParticipantID, + RoomID: data.RoomID, ServiceName: "telemost", Credentials: data.Credentials, SdkInfo: SdkInfo{ @@ -795,8 +844,12 @@ func getYandexCreds(link string) (string, string, string, error) { } if debug { - b, _ := json.MarshalIndent(req1, "", " ") - log.Printf("Sending HELLO:\n%s", string(b)) + b, err2 := json.MarshalIndent(req1, "", " ") + if err2 != nil { + log.Printf("Failed to marshal HELLO: %v", err2) + } else { + log.Printf("Sending HELLO:\n%s", string(b)) + } } if err := conn.WriteJSON(req1); err != nil { @@ -873,7 +926,7 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net. func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.PacketConn, connchan chan<- net.PacketConn, okchan chan<- struct{}, c chan<- error) { time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond) - var err error = nil + var err error defer func() { c <- err }() dtlsctx, dtlscancel := context.WithCancel(ctx) defer dtlscancel() @@ -1008,7 +1061,7 @@ type turnParams struct { func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, conn2 net.PacketConn, c chan<- error) { time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond) - var err error = nil + var err error defer func() { c <- err }() user, pass, url, err1 := turnParams.getCreds(turnParams.link) if err1 != nil { @@ -1028,13 +1081,13 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD } var turnServerAddr string turnServerAddr = net.JoinHostPort(urlhost, urlport) - turnServerUdpAddr, err1 := net.ResolveUDPAddr("udp", turnServerAddr) + turnServerUDPAddr, err1 := net.ResolveUDPAddr("udp", turnServerAddr) if err1 != nil { err = fmt.Errorf("failed to resolve TURN server address: %s", err1) return } - turnServerAddr = turnServerUdpAddr.String() - fmt.Println(turnServerUdpAddr.IP) + turnServerAddr = turnServerUDPAddr.String() + fmt.Println(turnServerUDPAddr.IP) // Dial TURN Server var cfg *turn.ClientConfig var turnConn net.PacketConn @@ -1042,7 +1095,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD ctx1, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if turnParams.udp { - conn, err2 := net.DialUDP("udp", nil, turnServerUdpAddr) // nolint: noctx + conn, err2 := net.DialUDP("udp", nil, turnServerUDPAddr) // nolint: noctx if err2 != nil { err = fmt.Errorf("failed to connect to TURN server: %s", err2) return @@ -1514,7 +1567,7 @@ func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAd if err != nil { log.Panicf("TCP listen: %s", err) } - context.AfterFunc(ctx, func() { listener.Close() }) + context.AfterFunc(ctx, func() { _ = listener.Close() }) log.Printf("TCP mode: listening on %s (round-robin across %d sessions)", listenAddr, numSessions) var wgConn sync.WaitGroup @@ -1535,20 +1588,20 @@ func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAd sess := pool.pick() if sess == nil || sess.IsClosed() { log.Printf("No active sessions, rejecting connection") - tcpConn.Close() + _ = tcpConn.Close() continue } wgConn.Add(1) go func(tc net.Conn, s *smux.Session) { defer wgConn.Done() - defer tc.Close() + defer func() { _ = tc.Close() }() stream, err := s.OpenStream() if err != nil { log.Printf("smux open stream error: %s", err) return } - defer stream.Close() + defer func() { _ = stream.Close() }() pipe(ctx, tc, stream) }(tcpConn, sess) } @@ -1625,31 +1678,31 @@ func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr) ( urlport = tp.port } turnServerAddr := net.JoinHostPort(urlhost, urlport) - turnServerUdpAddr, err := net.ResolveUDPAddr("udp", turnServerAddr) + turnServerUDPAddr, err := net.ResolveUDPAddr("udp", turnServerAddr) if err != nil { return nil, nil, fmt.Errorf("resolve TURN addr: %w", err) } - turnServerAddr = turnServerUdpAddr.String() + turnServerAddr = turnServerUDPAddr.String() // 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 nil, nil, fmt.Errorf("dial TURN (udp): %w", err) + c, err1 := net.DialUDP("udp", nil, turnServerUDPAddr) + if err1 != nil { + return nil, nil, fmt.Errorf("dial TURN (udp): %w", err1) } - cleanupFns = append(cleanupFns, func() { conn.Close() }) - turnConn = &connectedUDPConn{conn} + cleanupFns = append(cleanupFns, func() { _ = c.Close() }) + turnConn = &connectedUDPConn{c} } else { var d net.Dialer - conn, err := d.DialContext(ctx1, "tcp", turnServerAddr) - if err != nil { - return nil, nil, fmt.Errorf("dial TURN (tcp): %w", err) + c, err1 := d.DialContext(ctx1, "tcp", turnServerAddr) + if err1 != nil { + return nil, nil, fmt.Errorf("dial TURN (tcp): %w", err1) } - cleanupFns = append(cleanupFns, func() { conn.Close() }) - turnConn = turn.NewSTUNConn(conn) + cleanupFns = append(cleanupFns, func() { _ = c.Close() }) + turnConn = turn.NewSTUNConn(c) } // 3. Create TURN client and allocate relay @@ -1683,7 +1736,7 @@ func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr) ( cleanup() return nil, nil, fmt.Errorf("TURN allocate: %w", err) } - cleanupFns = append(cleanupFns, func() { relayConn.Close() }) + cleanupFns = append(cleanupFns, func() { _ = relayConn.Close() }) log.Printf("relayed-address=%s", relayConn.LocalAddr().String()) // 4. Establish DTLS over TURN relay @@ -1708,11 +1761,11 @@ func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr) ( ctx2, cancel2 := context.WithTimeout(ctx, 30*time.Second) defer cancel2() if err = dtlsConn.HandshakeContext(ctx2); err != nil { - dtlsConn.Close() + _ = dtlsConn.Close() cleanup() return nil, nil, fmt.Errorf("DTLS handshake: %w", err) } - cleanupFns = append(cleanupFns, func() { dtlsConn.Close() }) + cleanupFns = append(cleanupFns, func() { _ = dtlsConn.Close() }) log.Printf("DTLS connection established") // 5. Create KCP session over DTLS @@ -1721,7 +1774,7 @@ func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr) ( cleanup() return nil, nil, fmt.Errorf("KCP session: %w", err) } - cleanupFns = append(cleanupFns, func() { kcpSess.Close() }) + cleanupFns = append(cleanupFns, func() { _ = kcpSess.Close() }) log.Printf("KCP session established") // 6. Create smux client session over KCP @@ -1730,7 +1783,7 @@ func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr) ( cleanup() return nil, nil, fmt.Errorf("smux client: %w", err) } - cleanupFns = append(cleanupFns, func() { smuxSess.Close() }) + cleanupFns = append(cleanupFns, func() { _ = smuxSess.Close() }) log.Printf("smux session established") return smuxSess, cleanup, nil @@ -1760,8 +1813,12 @@ func (r *relayPacketConn) SetWriteDeadline(t time.Time) error { return r.relay.S 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()) + if err := c1.SetDeadline(time.Now()); err != nil { + log.Printf("pipe: failed to set deadline c1: %v", err) + } + if err := c2.SetDeadline(time.Now()); err != nil { + log.Printf("pipe: failed to set deadline c2: %v", err) + } }) var wg sync.WaitGroup @@ -1769,14 +1826,22 @@ func pipe(ctx context.Context, c1, c2 net.Conn) { go func() { defer wg.Done() defer cancel() - _, _ = io.Copy(c1, c2) + if _, err := io.Copy(c1, c2); err != nil { + log.Printf("pipe: c1<-c2 copy error: %v", err) + } }() go func() { defer wg.Done() defer cancel() - _, _ = io.Copy(c2, c1) + if _, err := io.Copy(c2, c1); err != nil { + log.Printf("pipe: c2<-c1 copy error: %v", err) + } }() wg.Wait() - _ = c1.SetDeadline(time.Time{}) - _ = c2.SetDeadline(time.Time{}) + if err := c1.SetDeadline(time.Time{}); err != nil { + log.Printf("pipe: failed to reset deadline c1: %v", err) + } + if err := c2.SetDeadline(time.Time{}); err != nil { + log.Printf("pipe: failed to reset deadline c2: %v", err) + } } diff --git a/server/main.go b/server/main.go index ad4b368..ade25a3 100644 --- a/server/main.go +++ b/server/main.go @@ -167,7 +167,7 @@ func handleUDPConnection(ctx context.Context, conn net.Conn, connectAddr string) return } - if err1 := serverConn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + if err1 = serverConn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { log.Printf("Failed: %s", err1) return } @@ -198,7 +198,7 @@ func handleUDPConnection(ctx context.Context, conn net.Conn, connectAddr string) return } - if err1 := conn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { + if err1 = conn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil { log.Printf("Failed: %s", err1) return } @@ -221,7 +221,7 @@ func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr str log.Printf("KCP session error: %s", err) return } - defer kcpSess.Close() + defer func() { _ = kcpSess.Close() }() log.Printf("KCP session established (server)") // 2. Create smux server session over KCP @@ -230,7 +230,7 @@ func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr str log.Printf("smux server error: %s", err) return } - defer smuxSess.Close() + defer func() { _ = smuxSess.Close() }() log.Printf("smux session established (server)") // 3. Accept smux streams and forward to backend via TCP @@ -249,7 +249,7 @@ func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr str wg.Add(1) go func(s *smux.Stream) { defer wg.Done() - defer s.Close() + defer func() { _ = s.Close() }() // Connect to backend (Xray/VLESS) backendConn, err := net.DialTimeout("tcp", connectAddr, 10*time.Second) @@ -257,7 +257,7 @@ func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr str log.Printf("backend dial error: %s", err) return } - defer backendConn.Close() + defer func() { _ = backendConn.Close() }() // Bidirectional copy pipeConn(ctx, s, backendConn) @@ -270,8 +270,12 @@ func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr str 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()) + if err := c1.SetDeadline(time.Now()); err != nil { + log.Printf("pipeConn: failed to set deadline c1: %v", err) + } + if err := c2.SetDeadline(time.Now()); err != nil { + log.Printf("pipeConn: failed to set deadline c2: %v", err) + } }) var wg sync.WaitGroup @@ -279,14 +283,22 @@ func pipeConn(ctx context.Context, c1, c2 net.Conn) { go func() { defer wg.Done() defer cancel() - _, _ = io.Copy(c1, c2) + if _, err := io.Copy(c1, c2); err != nil { + log.Printf("pipeConn: c1<-c2 copy error: %v", err) + } }() go func() { defer wg.Done() defer cancel() - _, _ = io.Copy(c2, c1) + if _, err := io.Copy(c2, c1); err != nil { + log.Printf("pipeConn: c2<-c1 copy error: %v", err) + } }() wg.Wait() - _ = c1.SetDeadline(time.Time{}) - _ = c2.SetDeadline(time.Time{}) + if err := c1.SetDeadline(time.Time{}); err != nil { + log.Printf("pipeConn: failed to reset deadline c1: %v", err) + } + if err := c2.SetDeadline(time.Time{}); err != nil { + log.Printf("pipeConn: failed to reset deadline c2: %v", err) + } } diff --git a/tcputil/tcputil.go b/tcputil/tcputil.go index f36bbdc..4e37ea3 100644 --- a/tcputil/tcputil.go +++ b/tcputil/tcputil.go @@ -52,14 +52,17 @@ func (d *DtlsPacketConn) SetWriteDeadline(t time.Time) error { func NewKCPOverDTLS(dtlsConn net.Conn, isServer bool) (*kcp.UDPSession, error) { pc := NewDtlsPacketConn(dtlsConn) - block, _ := kcp.NewNoneBlockCrypt(nil) // DTLS already encrypts + block, err := kcp.NewNoneBlockCrypt(nil) // DTLS already encrypts + if err != nil { + return nil, err + } 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) + var listener *kcp.Listener + listener, err = kcp.ServeConn(block, 0, 0, pc) if err != nil { return nil, err } From 6949cbd275bccd06d0eb6132ba01f32beb74bfd9 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Sat, 4 Apr 2026 17:07:31 +0700 Subject: [PATCH 06/14] Update .golangci.yml --- .golangci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 68d98b3..c209c83 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,5 @@ +version: 1 + linters: enable: - errcheck From 8a4d2e57a2964eb859dbfcdc2476094d3170e85e Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Sat, 4 Apr 2026 17:30:09 +0700 Subject: [PATCH 07/14] Update .golangci.yml --- .golangci.yml | 97 ++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index c209c83..578f6e1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,53 +1,56 @@ -version: 1 - +version: "2" +run: + tests: true linters: enable: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - typecheck - - unused - - revive - - misspell - bodyclose - -linters-settings: - errcheck: - check-blank: true - check-type-assertions: true - - govet: - enable-all: true - disable: - - fieldalignment - - revive: - rules: - - name: blank-imports - - name: dot-imports - - name: error-return - - name: error-strings - - name: error-naming - - name: exported - - name: if-return - - name: increment-decrement - - name: var-naming - - name: var-declaration - - name: package-comments - - name: range - - name: receiver-naming - - name: time-naming - - name: unexported-return - - name: indent-error-flow - - name: errorf - -run: - timeout: 5m - tests: true - + - misspell + - revive + settings: + errcheck: + check-type-assertions: true + check-blank: true + govet: + disable: + - fieldalignment + enable-all: true + revive: + rules: + - name: blank-imports + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ issues: - exclude-use-default: true max-issues-per-linter: 0 max-same-issues: 0 +formatters: + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ From 44b91d7eeeb6c2a44f5cecfa50c5120c8237d844 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Mon, 6 Apr 2026 12:09:43 +0700 Subject: [PATCH 08/14] fix: Rename tcp mode into vless --- README.md | 17 ++++++++--------- client/main.go | 20 ++++++++++---------- docker-entrypoint.sh | 8 ++++---- server/main.go | 18 +++++++++--------- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 53d4eef..bf1c1fb 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down С помощью опции `-turn` можно указать адрес TURN сервера вручную. Это должен быть сервер ВК, Макса или Одноклассников (ссылка вк) или Яндекса (ссылка яндекса). Возможно потом составлю список. -Если не работает TCP, попробуйте добавить флаг `-udp`. +Если не работает VLESS, попробуйте добавить флаг `-udp`. Добавьте флаг `-n 1` для более стабильного подключения в 1 поток (ограничение 5 Мбит/с для ВК) @@ -330,29 +330,29 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down -## VLESS (TCP-режим) +## VLESS-режим -Если WireGuard блокируется DPI, можно использовать VLESS через флаг `-tcp`. В этом режиме вместо UDP-пакетов пробрасываются TCP-соединения через TURN-туннель с помощью KCP и smux. +Если WireGuard блокируется DPI, можно использовать VLESS через флаг `-vless`. В этом режиме вместо UDP-пакетов пробрасываются TCP-соединения через TURN-туннель с помощью KCP и smux. ### Настройка 1. На VPS установить Xray с VLESS inbound -2. Запустить `server` с флагом `-tcp` -3. На клиенте запустить `client` с флагом `-tcp` +2. Запустить `server` с флагом `-vless` +3. На клиенте запустить `client` с флагом `-vless` 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 +./server -listen 0.0.0.0:56000 -connect 127.0.0.1:443 -vless ``` #### Docker ``` -docker run -p 56000:56000/udp -e CONNECT_ADDR=127.0.0.1:443 -e TCP_MODE=true vk-turn-proxy +docker run -p 56000:56000/udp -e CONNECT_ADDR=127.0.0.1:443 -e VLESS_MODE=true vk-turn-proxy ``` ### Клиент ``` -./client -peer :56000 -vk-link -listen 127.0.0.1:9000 -tcp +./client -peer :56000 -vk-link -listen 127.0.0.1:9000 -vless ```
@@ -442,7 +442,6 @@ Xray сервер (config.json)
-> **Важно:** В TCP-режиме используется один TURN-поток. Для VK это ограничивает скорость ~5 Мбит/с. ## Direct mode diff --git a/client/main.go b/client/main.go index 080b0b6..222f34e 100644 --- a/client/main.go +++ b/client/main.go @@ -1366,7 +1366,7 @@ func main() { //nolint:cyclop n := flag.Int("n", 0, "connections to TURN (default 10 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") + vlessMode := flag.Bool("vless", false, "VLESS mode: forward TCP connections (for VLESS) instead of UDP packets") flag.Parse() if *peerAddr == "" { log.Panicf("Need peer address!") @@ -1416,8 +1416,8 @@ func main() { //nolint:cyclop getCreds: poolCreds(getCreds, 1), } - if *tcpMode { - runTCPMode(ctx, params, peer, *listen, *n) + if *vlessMode { + runVLESSMode(ctx, params, peer, *listen, *n) return } @@ -1530,8 +1530,8 @@ func (p *sessionPool) count() int { return len(p.sessions) } -// runTCPMode implements TCP forwarding with round-robin across N TURN sessions. -func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAddr string, numSessions int) { +// runVLESSMode implements TCP forwarding with round-robin across N TURN sessions. +func runVLESSMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAddr string, numSessions int) { pool := &sessionPool{} // Start N session maintainers with staggered startup @@ -1545,12 +1545,12 @@ func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAd return case <-time.After(time.Duration(id) * 300 * time.Millisecond): } - maintainTCPSession(ctx, tp, peer, id, pool) + maintainVLESSSession(ctx, tp, peer, id, pool) }(i) } // Wait for at least one session - log.Printf("TCP mode: waiting for sessions to connect (total: %d)...", numSessions) + log.Printf("VLESS mode: waiting for sessions to connect (total: %d)...", numSessions) for { select { case <-ctx.Done(): @@ -1568,7 +1568,7 @@ func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAd log.Panicf("TCP listen: %s", err) } context.AfterFunc(ctx, func() { _ = listener.Close() }) - log.Printf("TCP mode: listening on %s (round-robin across %d sessions)", listenAddr, numSessions) + log.Printf("VLESS mode: listening on %s (round-robin across %d sessions)", listenAddr, numSessions) var wgConn sync.WaitGroup for { @@ -1607,8 +1607,8 @@ func runTCPMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listenAd } } -// maintainTCPSession keeps one TURN+DTLS+KCP+smux session alive, reconnecting on failure. -func maintainTCPSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, id int, pool *sessionPool) { +// maintainVLESSSession keeps one TURN+DTLS+KCP+smux session alive, reconnecting on failure. +func maintainVLESSSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, id int, pool *sessionPool) { for { select { case <-ctx.Done(): diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index c55b63e..941bf24 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -3,9 +3,9 @@ set -e CONNECT="${CONNECT_ADDR:?CONNECT_ADDR is required}" -TCP_FLAG="" -if [ "${TCP_MODE}" = "true" ]; then - TCP_FLAG="-tcp" +VLESS_FLAG="" +if [ "${VLESS_MODE}" = "true" ]; then + VLESS_FLAG="-vless" fi -exec ./vk-turn-proxy -listen 0.0.0.0:56000 -connect "$CONNECT" $TCP_FLAG +exec ./vk-turn-proxy -listen 0.0.0.0:56000 -connect "$CONNECT" $VLESS_FLAG diff --git a/server/main.go b/server/main.go index ade25a3..b6a789d 100644 --- a/server/main.go +++ b/server/main.go @@ -23,7 +23,7 @@ import ( 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") + vlessMode := flag.Bool("vless", false, "VLESS mode: forward TCP connections (for VLESS) instead of UDP packets") flag.Parse() ctx, cancel := context.WithCancel(context.Background()) @@ -113,8 +113,8 @@ func main() { cancel1() log.Println("Handshake done") - if *tcpMode { - handleTCPConnection(ctx, dtlsConn, *connect) + if *vlessMode { + handleVLESSConnection(ctx, dtlsConn, *connect) } else { handleUDPConnection(ctx, conn, *connect) } @@ -212,16 +212,16 @@ func handleUDPConnection(ctx context.Context, conn net.Conn, connectAddr string) wg.Wait() } -// handleTCPConnection creates a KCP+smux session over DTLS and forwards +// handleVLESSConnection 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) { +func handleVLESSConnection(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 func() { _ = kcpSess.Close() }() + defer kcpSess.Close() log.Printf("KCP session established (server)") // 2. Create smux server session over KCP @@ -230,7 +230,7 @@ func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr str log.Printf("smux server error: %s", err) return } - defer func() { _ = smuxSess.Close() }() + defer smuxSess.Close() log.Printf("smux session established (server)") // 3. Accept smux streams and forward to backend via TCP @@ -249,7 +249,7 @@ func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr str wg.Add(1) go func(s *smux.Stream) { defer wg.Done() - defer func() { _ = s.Close() }() + defer s.Close() // Connect to backend (Xray/VLESS) backendConn, err := net.DialTimeout("tcp", connectAddr, 10*time.Second) @@ -257,7 +257,7 @@ func handleTCPConnection(ctx context.Context, dtlsConn net.Conn, connectAddr str log.Printf("backend dial error: %s", err) return } - defer func() { _ = backendConn.Close() }() + defer backendConn.Close() // Bidirectional copy pipeConn(ctx, s, backendConn) From f66e228d965614aa2160e7a4291a5b32d3f8cb91 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Mon, 6 Apr 2026 12:16:19 +0700 Subject: [PATCH 09/14] small README change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf1c1fb..2c93e2c 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down С помощью опции `-turn` можно указать адрес TURN сервера вручную. Это должен быть сервер ВК, Макса или Одноклассников (ссылка вк) или Яндекса (ссылка яндекса). Возможно потом составлю список. -Если не работает VLESS, попробуйте добавить флаг `-udp`. +Если не работает TCP, попробуйте добавить флаг `-udp`. Добавьте флаг `-n 1` для более стабильного подключения в 1 поток (ограничение 5 Мбит/с для ВК) From 79d46b2e65351ae0011993fc79ecaf39e61b2b87 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Wed, 8 Apr 2026 18:53:25 +0700 Subject: [PATCH 10/14] fix: handle error returns and unsafe type assertions in errcheck linter --- .golangci.yml | 9 ++ client/main.go | 231 ++++++++++++++++++++++++++------------- client/manual_captcha.go | 4 +- server/main.go | 1 - 4 files changed, 168 insertions(+), 77 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 578f6e1..db48597 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -10,6 +10,11 @@ linters: errcheck: check-type-assertions: true check-blank: true + exclude-functions: + - (net.PacketConn).WriteTo + - (net.Conn).Write + - encoding/json.MarshalIndent + - (*github.com/pion/dtls/v3.Conn).SetDeadline govet: disable: - fieldalignment @@ -47,6 +52,10 @@ linters: issues: max-issues-per-linter: 0 max-same-issues: 0 + exclude-rules: + - linters: + - errcheck + source: "doRequest|packetPool\\.Get" formatters: exclusions: generated: lax diff --git a/client/main.go b/client/main.go index 73ba7d0..fd8ecf6 100644 --- a/client/main.go +++ b/client/main.go @@ -236,7 +236,7 @@ type VkCaptchaError struct { ErrorMsg string CaptchaSid string CaptchaImg string - RedirectUri string + RedirectURI string IsSoundCaptchaAvailable bool SessionToken string CaptchaTs string @@ -244,58 +244,96 @@ type VkCaptchaError struct { } func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError { - codeFloat, _ := errData["error_code"].(float64) - code := int(codeFloat) - - redirectUri, _ := errData["redirect_uri"].(string) - captchaSid, _ := errData["captcha_sid"].(string) - if captchaSid == "" { - if sidNum, ok := errData["captcha_sid"].(float64); ok { - captchaSid = fmt.Sprintf("%.0f", sidNum) - } - } - - captchaImg, _ := errData["captcha_img"].(string) - errorMsg, _ := errData["error_msg"].(string) - - var sessionToken string - if redirectUri != "" { - if parsed, err := neturl.Parse(redirectUri); err == nil { - sessionToken = parsed.Query().Get("session_token") - } - } - - isSound, _ := errData["is_sound_captcha_available"].(bool) - - var captchaTs string - if tsFloat, ok := errData["captcha_ts"].(float64); ok { - captchaTs = fmt.Sprintf("%.0f", tsFloat) - } else if tsStr, ok := errData["captcha_ts"].(string); ok { - captchaTs = tsStr - } - - var captchaAttempt string - if attFloat, ok := errData["captcha_attempt"].(float64); ok { - captchaAttempt = fmt.Sprintf("%.0f", attFloat) - } else if attStr, ok := errData["captcha_attempt"].(string); ok { - captchaAttempt = attStr - } - - return &VkCaptchaError{ - ErrorCode: code, - ErrorMsg: errorMsg, - CaptchaSid: captchaSid, - CaptchaImg: captchaImg, - RedirectUri: redirectUri, - IsSoundCaptchaAvailable: isSound, - SessionToken: sessionToken, - CaptchaTs: captchaTs, - CaptchaAttempt: captchaAttempt, + // Extract error_code + codeFloat, ok := errData["error_code"].(float64) + if !ok { + log.Printf("missing error_code in captcha error data") + return nil + } + code := int(codeFloat) + + // Extract redirect_uri + RedirectURI, ok := errData["redirect_uri"].(string) + if !ok { + log.Printf("missing redirect_uri in captcha error data") + return nil + } + + // Extract captcha_sid + captchaSid, ok := errData["captcha_sid"].(string) + if !ok { + // try numeric + if sidNum, ok2 := errData["captcha_sid"].(float64); ok2 { + captchaSid = fmt.Sprintf("%.0f", sidNum) + } else { + log.Printf("missing captcha_sid in captcha error data") + return nil + } + } + + // Extract captcha_img + captchaImg, ok := errData["captcha_img"].(string) + if !ok { + log.Printf("missing captcha_img in captcha error data") + return nil + } + + // Extract error_msg + errorMsg, ok := errData["error_msg"].(string) + if !ok { + log.Printf("missing error_msg in captcha error data") + return nil + } + + // Extract session token if redirect_uri present + var sessionToken string + if RedirectURI != "" { + if parsed, err := neturl.Parse(RedirectURI); err == nil { + sessionToken = parsed.Query().Get("session_token") + } else { + log.Printf("failed to parse redirect_uri: %v", err) + return nil + } + } + + // Extract is_sound_captcha_available + isSound, ok := errData["is_sound_captcha_available"].(bool) + if !ok { + isSound = false + } + + // Extract captcha_ts + var captchaTs string + if tsFloat, ok := errData["captcha_ts"].(float64); ok { + captchaTs = fmt.Sprintf("%.0f", tsFloat) + } else if tsStr, ok := errData["captcha_ts"].(string); ok { + captchaTs = tsStr + } + + // Extract captcha_attempt + var captchaAttempt string + if attFloat, ok := errData["captcha_attempt"].(float64); ok { + captchaAttempt = fmt.Sprintf("%.0f", attFloat) + } else if attStr, ok := errData["captcha_attempt"].(string); ok { + captchaAttempt = attStr + } + + // Build VkCaptchaError + return &VkCaptchaError{ + ErrorCode: code, + ErrorMsg: errorMsg, + CaptchaSid: captchaSid, + CaptchaImg: captchaImg, + RedirectURI: RedirectURI, + IsSoundCaptchaAvailable: isSound, + SessionToken: sessionToken, + CaptchaTs: captchaTs, + CaptchaAttempt: captchaAttempt, } } func (e *VkCaptchaError) IsCaptchaError() bool { - return e.ErrorCode == 14 && e.RedirectUri != "" && e.SessionToken != "" + return e.ErrorCode == 14 && e.RedirectURI != "" && e.SessionToken != "" } func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, client tlsclient.HttpClient, profile Profile) (string, error) { @@ -304,11 +342,11 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in if captchaErr.SessionToken == "" { return "", fmt.Errorf("no session_token in redirect_uri for auto-solve") } - if captchaErr.RedirectUri == "" { + if captchaErr.RedirectURI == "" { return "", fmt.Errorf("no redirect_uri for auto-solve") } - powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectUri, client, profile) + powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectURI, client, profile) if err != nil { return "", fmt.Errorf("failed to fetch PoW input: %w", err) } @@ -327,14 +365,14 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in return successToken, nil } -func fetchPowInput(ctx context.Context, redirectUri string, client tlsclient.HttpClient, profile Profile) (string, int, error) { - parsedURL, err := neturl.Parse(redirectUri) +func fetchPowInput(ctx context.Context, RedirectURI string, client tlsclient.HttpClient, profile Profile) (string, int, error) { + parsedURL, err := neturl.Parse(RedirectURI) if err != nil { return "", 0, err } domain := parsedURL.Hostname() - req, err := fhttp.NewRequestWithContext(ctx, "GET", redirectUri, nil) + req, err := fhttp.NewRequestWithContext(ctx, "GET", RedirectURI, nil) if err != nil { return "", 0, err } @@ -394,7 +432,10 @@ func solvePoW(powInput string, difficulty int) string { func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamID int, client tlsclient.HttpClient, profile Profile) (string, error) { vkReq := func(method string, postData string) (map[string]interface{}, error) { reqURL := "https://api.vk.ru/method/" + method + "?v=5.131" - parsedURL, _ := neturl.Parse(reqURL) + parsedURL, err := neturl.Parse(reqURL) + if err != nil { + return nil, fmt.Errorf("parse request URL: %w", err) + } domain := parsedURL.Hostname() req, err := fhttp.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(postData)) @@ -748,7 +789,10 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede log.Printf("[STREAM %d] [VK Auth] Connecting Identity - Name: %s | User-Agent: %s", streamID, name, profile.UserAgent) doRequest := func(data string, url string) (resp map[string]interface{}, err error) { - parsedURL, _ := neturl.Parse(url) + parsedURL, err := neturl.Parse(url) + if err != nil { + return nil, fmt.Errorf("parse request URL: %w", err) + } domain := parsedURL.Hostname() req, err := fhttp.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer([]byte(data))) @@ -808,7 +852,10 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede // Token 1 -> getCallPreview data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&fields=photo_200&access_token=%s", link, token1) - _, _ = doRequest(data, "https://api.vk.ru/method/calls.getCallPreview?v=5.275&client_id="+creds.ClientID) + _, err = doRequest(data, "https://api.vk.ru/method/calls.getCallPreview?v=5.275&client_id="+creds.ClientID) + if err != nil { + log.Printf("[STREAM %d] [VK Auth] Warning: getCallPreview failed: %v", streamID, err) + } vkDelayRandom(200, 400) @@ -833,7 +880,7 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede if attempt < maxAutoAttempts { // Auto Solve Attempts - if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" { + if captchaErr.SessionToken != "" && captchaErr.RedirectURI != "" { successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile) if solveErr != nil { log.Printf("[STREAM %d] [Captcha] Auto solve failed: %v", streamID, solveErr) @@ -857,8 +904,8 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede go func() { var t, k string var e error - if captchaErr.RedirectUri != "" { - t, e = solveCaptchaViaProxy(captchaErr.RedirectUri, dialer) + if captchaErr.RedirectURI != "" { + t, e = solveCaptchaViaProxy(captchaErr.RedirectURI, dialer) } else if captchaErr.CaptchaImg != "" { k, e = solveCaptchaViaHTTP(captchaErr.CaptchaImg) } else { @@ -950,7 +997,10 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede if err != nil { return "", "", "", err } - token3 := resp["session_key"].(string) + token3, ok := resp["session_key"].(string) + if !ok { + return "", "", "", fmt.Errorf("missing session_key in response: %v", resp) + } vkDelayRandom(100, 150) @@ -961,11 +1011,26 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede return "", "", "", err } - ts := resp["turn_server"].(map[string]interface{}) - user := ts["username"].(string) - pass := ts["credential"].(string) - urls := ts["urls"].([]interface{}) - urlStr := urls[0].(string) + tsRaw, ok := resp["turn_server"].(map[string]interface{}) + if !ok { + return "", "", "", fmt.Errorf("missing turn_server in response: %v", resp) + } + user, ok := tsRaw["username"].(string) + if !ok { + return "", "", "", fmt.Errorf("missing username in turn_server") + } + pass, ok := tsRaw["credential"].(string) + if !ok { + return "", "", "", fmt.Errorf("missing credential in turn_server") + } + urlsRaw, ok := tsRaw["urls"].([]interface{}) + if !ok || len(urlsRaw) == 0 { + return "", "", "", fmt.Errorf("missing or empty urls in turn_server") + } + urlStr, ok := urlsRaw[0].(string) + if !ok { + return "", "", "", fmt.Errorf("turn server url is not a string") + } clean := strings.Split(urlStr, "?")[0] address := strings.TrimPrefix(strings.TrimPrefix(clean, "turn:"), "turns:") @@ -1315,7 +1380,7 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net. func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.PacketConn, inboundChan <-chan *UDPPacket, connchan chan<- net.PacketConn, okchan chan<- struct{}, streamID int) error { time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond) - var err error + dtlsctx, dtlscancel := context.WithCancel(ctx) defer dtlscancel() @@ -1353,7 +1418,9 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa wg := sync.WaitGroup{} wg.Add(1) context.AfterFunc(dtlsctx, func() { - _ = dtlsConn.SetDeadline(time.Now()) + if err := dtlsConn.SetDeadline(time.Now()); err != nil { + log.Printf("[STREAM %d] Warning: SetDeadline failed: %v", streamID, err) + } }) go func() { @@ -1381,7 +1448,11 @@ func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.Pa // Send back to the active WG client if peerAddr := activeLocalPeer.Load(); peerAddr != nil { - _, _ = listenConn.WriteTo(buf[:n], peerAddr.(net.Addr)) + if addr, ok := peerAddr.(net.Addr); ok { + if _, err := listenConn.WriteTo(buf[:n], addr); err != nil { + log.Printf("[STREAM %d] failed to forward packet to local peer: %v", streamID, err) + } + } } } }() @@ -1574,9 +1645,10 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD continue } - _, err1 = conn2.WriteTo(buf[:n], addr1.(net.Addr)) - if err1 != nil { - return + if addr, ok := addr1.(net.Addr); ok { + if _, err := conn2.WriteTo(buf[:n], addr); err != nil { + return + } } } }() @@ -1766,7 +1838,12 @@ func main() { go func() { for { - pkt := packetPool.Get().(*UDPPacket) + pktIface := packetPool.Get() + pkt, ok := pktIface.(*UDPPacket) + if !ok { + log.Printf("packetPool returned unexpected type: %T", pktIface) + continue + } nRead, addr, err := listenConn.ReadFrom(pkt.Data) if err != nil { return @@ -1774,7 +1851,13 @@ func main() { // Save the local WireGuard peer address current := activeLocalPeer.Load() - if current == nil || current.(net.Addr).String() != addr.String() { + if current == nil { + activeLocalPeer.Store(addr) + } else if addrStr, ok := current.(net.Addr); ok { + if addrStr.String() != addr.String() { + activeLocalPeer.Store(addr) + } + } else { activeLocalPeer.Store(addr) } diff --git a/client/manual_captcha.go b/client/manual_captcha.go index 204240e..09b342a 100644 --- a/client/manual_captcha.go +++ b/client/manual_captcha.go @@ -514,8 +514,8 @@ func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string, }) mux.HandleFunc("/generic_proxy", func(w http.ResponseWriter, r *http.Request) { - targetAuthUrl := r.URL.Query().Get("proxy_url") - targetParsed, err := neturl.Parse(targetAuthUrl) + targetAuthURL := r.URL.Query().Get("proxy_url") + targetParsed, err := neturl.Parse(targetAuthURL) if err != nil || targetParsed.Host == "" { http.Error(w, "Bad URL", http.StatusBadRequest) return diff --git a/server/main.go b/server/main.go index f6c4d0f..123c101 100644 --- a/server/main.go +++ b/server/main.go @@ -20,7 +20,6 @@ import ( "github.com/xtaci/smux" ) -const idleTimeout = 2 * time.Minute func main() { listen := flag.String("listen", "0.0.0.0:56000", "listen on ip:port") From c87ccd05e3fffa18577fc020dd318fe3dfe21414 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Wed, 8 Apr 2026 19:25:41 +0700 Subject: [PATCH 11/14] Move .golangci to workflows --- .golangci.yml => .github/workflows/.golangci.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .golangci.yml => .github/workflows/.golangci.yml (100%) diff --git a/.golangci.yml b/.github/workflows/.golangci.yml similarity index 100% rename from .golangci.yml rename to .github/workflows/.golangci.yml From f278c65e6fe4f270d0a25b55a9520a0a8210d729 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Wed, 8 Apr 2026 19:43:33 +0700 Subject: [PATCH 12/14] Resolve gofmt formatting issues --- client/main.go | 172 ++++++++++++++++++++++++------------------------- server/main.go | 1 - 2 files changed, 86 insertions(+), 87 deletions(-) diff --git a/client/main.go b/client/main.go index 80f211b..1f0847e 100644 --- a/client/main.go +++ b/client/main.go @@ -245,91 +245,91 @@ type VkCaptchaError struct { } func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError { - // Extract error_code - codeFloat, ok := errData["error_code"].(float64) - if !ok { - log.Printf("missing error_code in captcha error data") - return nil - } - code := int(codeFloat) - - // Extract redirect_uri - RedirectURI, ok := errData["redirect_uri"].(string) - if !ok { - log.Printf("missing redirect_uri in captcha error data") - return nil - } - - // Extract captcha_sid - captchaSid, ok := errData["captcha_sid"].(string) - if !ok { - // try numeric - if sidNum, ok2 := errData["captcha_sid"].(float64); ok2 { - captchaSid = fmt.Sprintf("%.0f", sidNum) - } else { - log.Printf("missing captcha_sid in captcha error data") - return nil - } - } - - // Extract captcha_img - captchaImg, ok := errData["captcha_img"].(string) - if !ok { - log.Printf("missing captcha_img in captcha error data") - return nil - } - - // Extract error_msg - errorMsg, ok := errData["error_msg"].(string) - if !ok { - log.Printf("missing error_msg in captcha error data") - return nil - } - - // Extract session token if redirect_uri present - var sessionToken string - if RedirectURI != "" { - if parsed, err := neturl.Parse(RedirectURI); err == nil { - sessionToken = parsed.Query().Get("session_token") - } else { - log.Printf("failed to parse redirect_uri: %v", err) - return nil - } - } - - // Extract is_sound_captcha_available - isSound, ok := errData["is_sound_captcha_available"].(bool) - if !ok { - isSound = false - } - - // Extract captcha_ts - var captchaTs string - if tsFloat, ok := errData["captcha_ts"].(float64); ok { - captchaTs = fmt.Sprintf("%.0f", tsFloat) - } else if tsStr, ok := errData["captcha_ts"].(string); ok { - captchaTs = tsStr - } - - // Extract captcha_attempt - var captchaAttempt string - if attFloat, ok := errData["captcha_attempt"].(float64); ok { - captchaAttempt = fmt.Sprintf("%.0f", attFloat) - } else if attStr, ok := errData["captcha_attempt"].(string); ok { - captchaAttempt = attStr - } - - // Build VkCaptchaError - return &VkCaptchaError{ - ErrorCode: code, - ErrorMsg: errorMsg, - CaptchaSid: captchaSid, - CaptchaImg: captchaImg, - RedirectURI: RedirectURI, - IsSoundCaptchaAvailable: isSound, - SessionToken: sessionToken, - CaptchaTs: captchaTs, - CaptchaAttempt: captchaAttempt, + // Extract error_code + codeFloat, ok := errData["error_code"].(float64) + if !ok { + log.Printf("missing error_code in captcha error data") + return nil + } + code := int(codeFloat) + + // Extract redirect_uri + RedirectURI, ok := errData["redirect_uri"].(string) + if !ok { + log.Printf("missing redirect_uri in captcha error data") + return nil + } + + // Extract captcha_sid + captchaSid, ok := errData["captcha_sid"].(string) + if !ok { + // try numeric + if sidNum, ok2 := errData["captcha_sid"].(float64); ok2 { + captchaSid = fmt.Sprintf("%.0f", sidNum) + } else { + log.Printf("missing captcha_sid in captcha error data") + return nil + } + } + + // Extract captcha_img + captchaImg, ok := errData["captcha_img"].(string) + if !ok { + log.Printf("missing captcha_img in captcha error data") + return nil + } + + // Extract error_msg + errorMsg, ok := errData["error_msg"].(string) + if !ok { + log.Printf("missing error_msg in captcha error data") + return nil + } + + // Extract session token if redirect_uri present + var sessionToken string + if RedirectURI != "" { + if parsed, err := neturl.Parse(RedirectURI); err == nil { + sessionToken = parsed.Query().Get("session_token") + } else { + log.Printf("failed to parse redirect_uri: %v", err) + return nil + } + } + + // Extract is_sound_captcha_available + isSound, ok := errData["is_sound_captcha_available"].(bool) + if !ok { + isSound = false + } + + // Extract captcha_ts + var captchaTs string + if tsFloat, ok := errData["captcha_ts"].(float64); ok { + captchaTs = fmt.Sprintf("%.0f", tsFloat) + } else if tsStr, ok := errData["captcha_ts"].(string); ok { + captchaTs = tsStr + } + + // Extract captcha_attempt + var captchaAttempt string + if attFloat, ok := errData["captcha_attempt"].(float64); ok { + captchaAttempt = fmt.Sprintf("%.0f", attFloat) + } else if attStr, ok := errData["captcha_attempt"].(string); ok { + captchaAttempt = attStr + } + + // Build VkCaptchaError + return &VkCaptchaError{ + ErrorCode: code, + ErrorMsg: errorMsg, + CaptchaSid: captchaSid, + CaptchaImg: captchaImg, + RedirectURI: RedirectURI, + IsSoundCaptchaAvailable: isSound, + SessionToken: sessionToken, + CaptchaTs: captchaTs, + CaptchaAttempt: captchaAttempt, } } @@ -1003,7 +1003,7 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede } token3, ok := resp["session_key"].(string) if !ok { - return "", "", "", fmt.Errorf("missing session_key in response: %v", resp) + return "", "", "", fmt.Errorf("missing session_key in response: %v", resp) } vkDelayRandom(100, 150) diff --git a/server/main.go b/server/main.go index 123c101..36cd7cf 100644 --- a/server/main.go +++ b/server/main.go @@ -20,7 +20,6 @@ import ( "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") From 471236272971aa50a6a34e6c9ba2d5c3f1dd6031 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Wed, 8 Apr 2026 19:54:39 +0700 Subject: [PATCH 13/14] Resolve golangci issues --- server/main.go | 53 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/server/main.go b/server/main.go index 36cd7cf..729262b 100644 --- a/server/main.go +++ b/server/main.go @@ -48,7 +48,7 @@ func main() { // Generate a certificate and private key to secure the connection certificate, genErr := selfsign.GenerateSelfSigned() if genErr != nil { - panic(err) + panic(genErr) } // Prepare the configuration of the DTLS connection @@ -98,19 +98,18 @@ func main() { // Perform the handshake with a 30-second timeout ctx1, cancel1 := context.WithTimeout(ctx, 30*time.Second) + defer cancel1() + dtlsConn, ok := conn.(*dtls.Conn) if !ok { - log.Println("Type error") - cancel1() + log.Println("Type error: expected *dtls.Conn") return } log.Println("Start handshake") if err := dtlsConn.HandshakeContext(ctx1); err != nil { - log.Println(err) - cancel1() + log.Printf("Handshake failed: %v", err) return } - cancel1() log.Println("Handshake done") if *vlessMode { @@ -222,7 +221,11 @@ func handleVLESSConnection(ctx context.Context, dtlsConn net.Conn, connectAddr s log.Printf("KCP session error: %s", err) return } - defer kcpSess.Close() + defer func() { + if err := kcpSess.Close(); err != nil { + log.Printf("failed to close KCP session: %v", err) + } + }() log.Printf("KCP session established (server)") // 2. Create smux server session over KCP @@ -231,7 +234,11 @@ func handleVLESSConnection(ctx context.Context, dtlsConn net.Conn, connectAddr s log.Printf("smux server error: %s", err) return } - defer smuxSess.Close() + defer func() { + if err := smuxSess.Close(); err != nil { + log.Printf("failed to close smux session: %v", err) + } + }() log.Printf("smux session established (server)") // 3. Accept smux streams and forward to backend via TCP @@ -250,7 +257,12 @@ func handleVLESSConnection(ctx context.Context, dtlsConn net.Conn, connectAddr s wg.Add(1) go func(s *smux.Stream) { defer wg.Done() - defer s.Close() + + defer func() { + if err := s.Close(); err != nil && err != smux.ErrGoAway { + log.Printf("failed to close smux stream: %v", err) + } + }() // Connect to backend (Xray/VLESS) backendConn, err := net.DialTimeout("tcp", connectAddr, 10*time.Second) @@ -258,7 +270,11 @@ func handleVLESSConnection(ctx context.Context, dtlsConn net.Conn, connectAddr s log.Printf("backend dial error: %s", err) return } - defer backendConn.Close() + defer func() { + if err := backendConn.Close(); err != nil { + log.Printf("failed to close backend connection: %v", err) + } + }() // Bidirectional copy pipeConn(ctx, s, backendConn) @@ -270,6 +286,8 @@ func handleVLESSConnection(ctx context.Context, dtlsConn net.Conn, connectAddr s // pipeConn copies data bidirectionally between two connections. func pipeConn(ctx context.Context, c1, c2 net.Conn) { ctx2, cancel := context.WithCancel(ctx) + defer cancel() + context.AfterFunc(ctx2, func() { if err := c1.SetDeadline(time.Now()); err != nil { log.Printf("pipeConn: failed to set deadline c1: %v", err) @@ -281,25 +299,24 @@ func pipeConn(ctx context.Context, c1, c2 net.Conn) { var wg sync.WaitGroup wg.Add(2) + go func() { defer wg.Done() - defer cancel() if _, err := io.Copy(c1, c2); err != nil { log.Printf("pipeConn: c1<-c2 copy error: %v", err) } }() + go func() { defer wg.Done() - defer cancel() if _, err := io.Copy(c2, c1); err != nil { log.Printf("pipeConn: c2<-c1 copy error: %v", err) } }() + wg.Wait() - if err := c1.SetDeadline(time.Time{}); err != nil { - log.Printf("pipeConn: failed to reset deadline c1: %v", err) - } - if err := c2.SetDeadline(time.Time{}); err != nil { - log.Printf("pipeConn: failed to reset deadline c2: %v", err) - } + + // Reset deadlines + _ = c1.SetDeadline(time.Time{}) + _ = c2.SetDeadline(time.Time{}) } From 58a9b1e08147e92506f3fec21ccf3d8afe31e1f9 Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Thu, 9 Apr 2026 00:26:37 +0700 Subject: [PATCH 14/14] Resolve golangci and gofmt issues --- client/main.go | 19 +++++++++---------- go.mod | 2 ++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/client/main.go b/client/main.go index 9cdd3d8..6186914 100644 --- a/client/main.go +++ b/client/main.go @@ -432,7 +432,7 @@ func fetchCaptchaBootstrap(ctx context.Context, redirectURI string, client tlscl } domain := parsedURL.Hostname() - req, err := fhttp.NewRequestWithContext(ctx, "GET", RedirectURI, nil) + req, err := fhttp.NewRequestWithContext(ctx, "GET", redirectURI, nil) if err != nil { return nil, err } @@ -936,7 +936,7 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede solveErr = fmt.Errorf("missing fields for auto solve") } case captchaSolveModeSliderPOC: - if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" { + if captchaErr.SessionToken != "" && captchaErr.RedirectURI != "" { successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile, true) if solveErr != nil { log.Printf("[STREAM %d] [Captcha] Auto captcha slider POC failed: %v", streamID, solveErr) @@ -2215,14 +2215,13 @@ func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, i return nil, nil, fmt.Errorf("generate cert: %w", err) } 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) + dtlsConn, err := dtls.ClientWithOptions(dtlsPC, peer, + dtls.WithCertificates(certificate), + dtls.WithInsecureSkipVerify(true), + dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret), + dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256), + dtls.WithConnectionIDGenerator(dtls.OnlySendCIDGenerator()), + ) if err != nil { cleanup() return nil, nil, fmt.Errorf("DTLS client create: %w", err) diff --git a/go.mod b/go.mod index 6a797f8..e6e5929 100644 --- a/go.mod +++ b/go.mod @@ -46,4 +46,6 @@ require ( golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.35.0 // indirect golang.org/x/tools v0.43.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/grpc v1.80.0 // indirect )