Browse Source

Merge pull request #102 from Moroka8/main

feat: поддержка TCP-режима для VLESS
pull/131/head v1.8.0
cacggghp 2 months ago
committed by GitHub
parent
commit
d16367b031
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 65
      .github/workflows/.golangci.yml
  2. 6
      .github/workflows/ci.yml
  3. 114
      README.md
  4. 575
      client/main.go
  5. 4
      client/manual_captcha.go
  6. 7
      docker-entrypoint.sh
  7. 11
      go.mod
  8. 102
      go.sum
  9. 2
      routes.ps1
  10. 301
      server/main.go
  11. 103
      tcputil/tcputil.go

65
.github/workflows/.golangci.yml

@ -0,0 +1,65 @@
version: "2"
run:
tests: true
linters:
enable:
- bodyclose
- misspell
- revive
settings:
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
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:
max-issues-per-linter: 0
max-same-issues: 0
exclude-rules:
- linters:
- errcheck
source: "doRequest|packetPool\\.Get"
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

6
.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

114
README.md

@ -422,6 +422,120 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down
</details>
## VLESS-режим
Если WireGuard блокируется DPI, можно использовать VLESS через флаг `-vless`. В этом режиме вместо UDP-пакетов пробрасываются TCP-соединения через TURN-туннель с помощью KCP и smux.
### Настройка
1. На VPS установить Xray с VLESS inbound
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 -vless
```
#### Docker
```
docker run -p 56000:56000/udp -e CONNECT_ADDR=127.0.0.1:443 -e VLESS_MODE=true vk-turn-proxy
```
### Клиент
```
./client -peer <ip сервера>:56000 -vk-link <VK ссылка> -listen 127.0.0.1:9000 -vless
```
<details>
<summary>
Xray клиент (config.json)
</summary>
```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": "<UUID>",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "none"
}
}
]
}
```
</details>
<details>
<summary>
Xray сервер (config.json)
</summary>
```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"
}
}
]
}
```
</details>
## Direct mode
С флагом `-no-dtls` можно отправлять пакеты без обфускации DTLS и подключаться к обычным серверам Wireguard. Может привести к бану от вк/яндекса.

575
client/main.go

@ -33,6 +33,7 @@ import (
"github.com/bogdanfinn/tls-client/profiles"
"github.com/bschaatsbergen/dnsdialer"
"github.com/cacggghp/vk-turn-proxy/tcputil"
"github.com/cbeuw/connutil"
"github.com/google/uuid"
"github.com/gorilla/websocket"
@ -41,6 +42,7 @@ import (
"github.com/pion/logging"
"github.com/pion/transport/v4"
"github.com/pion/turn/v5"
"github.com/xtaci/smux"
)
type getCredsFunc func(ctx context.Context, link string, streamID int) (string, string, string, error)
@ -277,7 +279,7 @@ type VkCaptchaError struct {
ErrorMsg string
CaptchaSid string
CaptchaImg string
RedirectUri string
RedirectURI string
IsSoundCaptchaAvailable bool
SessionToken string
CaptchaTs string
@ -285,29 +287,65 @@ type VkCaptchaError struct {
}
func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError {
codeFloat, _ := errData["error_code"].(float64)
// 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)
redirectUri, _ := errData["redirect_uri"].(string)
captchaSid, _ := errData["captcha_sid"].(string)
if captchaSid == "" {
if sidNum, ok := errData["captcha_sid"].(float64); ok {
// 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
}
}
captchaImg, _ := errData["captcha_img"].(string)
errorMsg, _ := errData["error_msg"].(string)
// 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 {
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
}
}
isSound, _ := errData["is_sound_captcha_available"].(bool)
// 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)
@ -315,6 +353,7 @@ func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError {
captchaTs = tsStr
}
// Extract captcha_attempt
var captchaAttempt string
if attFloat, ok := errData["captcha_attempt"].(float64); ok {
captchaAttempt = fmt.Sprintf("%.0f", attFloat)
@ -322,12 +361,13 @@ func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError {
captchaAttempt = attStr
}
// Build VkCaptchaError
return &VkCaptchaError{
ErrorCode: code,
ErrorMsg: errorMsg,
CaptchaSid: captchaSid,
CaptchaImg: captchaImg,
RedirectUri: redirectUri,
RedirectURI: RedirectURI,
IsSoundCaptchaAvailable: isSound,
SessionToken: sessionToken,
CaptchaTs: captchaTs,
@ -336,7 +376,7 @@ func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError {
}
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, useSliderPOC bool) (string, error) {
@ -349,11 +389,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")
}
bootstrap, err := fetchCaptchaBootstrap(ctx, captchaErr.RedirectUri, client, profile)
bootstrap, err := fetchCaptchaBootstrap(ctx, captchaErr.RedirectURI, client, profile)
if err != nil {
return "", fmt.Errorf("failed to fetch captcha bootstrap: %w", err)
}
@ -385,14 +425,14 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in
return successToken, nil
}
func fetchCaptchaBootstrap(ctx context.Context, redirectUri string, client tlsclient.HttpClient, profile Profile) (*captchaBootstrap, error) {
parsedURL, err := neturl.Parse(redirectUri)
func fetchCaptchaBootstrap(ctx context.Context, redirectURI string, client tlsclient.HttpClient, profile Profile) (*captchaBootstrap, error) {
parsedURL, err := neturl.Parse(redirectURI)
if err != nil {
return nil, err
}
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
}
@ -435,7 +475,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))
@ -522,8 +565,8 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI
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)
@ -789,7 +832,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)))
@ -849,7 +895,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)
@ -878,7 +927,7 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede
switch solveMode {
case captchaSolveModeAuto:
if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" {
if captchaErr.SessionToken != "" && captchaErr.RedirectURI != "" {
successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile, false)
if solveErr != nil {
log.Printf("[STREAM %d] [Captcha] Auto captcha failed: %v", streamID, solveErr)
@ -887,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)
@ -909,8 +958,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 {
@ -968,12 +1017,12 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede
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
@ -988,7 +1037,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)
@ -999,11 +1051,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:")
@ -1116,7 +1183,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"`
@ -1125,8 +1192,8 @@ func getYandexCreds(link string) (string, string, string, error) {
}
type WSSData struct {
ParticipantId string
RoomId string
ParticipantID string
RoomID string
Credentials string
Wss string
}
@ -1163,8 +1230,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
@ -1172,8 +1242,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,
}
@ -1185,10 +1255,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)
@ -1214,8 +1291,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{
@ -1344,6 +1421,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)
dtlsctx, dtlscancel := context.WithCancel(ctx)
defer dtlscancel()
@ -1381,7 +1459,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() {
@ -1409,7 +1489,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)
}
}
}
}
}()
@ -1439,7 +1523,7 @@ type turnParams struct {
func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, conn2 net.PacketConn, streamID int, 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, urlTarget, err1 := turnParams.getCreds(ctx, turnParams.link, streamID)
if err1 != nil {
@ -1459,20 +1543,19 @@ 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()
turnServerAddr = turnServerUDPAddr.String()
var cfg *turn.ClientConfig
var turnConn net.PacketConn
var d net.Dialer
ctx1, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if turnParams.udp {
conn, err2 := net.DialUDP("udp", nil, turnServerUdpAddr)
conn, err2 := net.DialUDP("udp", nil, turnServerUDPAddr) // nolint: noctx
if err2 != nil {
err = fmt.Errorf("failed to connect to TURN server: %s", err2)
return
@ -1603,9 +1686,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
}
}
}
}()
@ -1714,6 +1798,7 @@ func main() {
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")
vlessMode := flag.Bool("vless", false, "VLESS mode: forward TCP connections (for VLESS) instead of UDP packets")
debugFlag := flag.Bool("debug", false, "enable debug logging")
manualCaptchaFlag := flag.Bool("manual-captcha", false, "skip auto captcha solving, use manual mode immediately")
flag.Parse()
@ -1772,6 +1857,11 @@ func main() {
getCreds: getCreds,
}
if *vlessMode {
runVLESSMode(ctx, params, peer, *listen, *n)
return
}
listenConn, err := net.ListenPacket("udp", *listen)
if err != nil {
log.Panicf("Failed to listen: %s", err)
@ -1792,7 +1882,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
@ -1800,7 +1895,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)
}
@ -1856,3 +1957,359 @@ func main() {
wg1.Wait()
}
// 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)
}
// 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
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):
}
maintainVLESSSession(ctx, tp, peer, id, pool)
}(i)
}
// Wait for at least one session
log.Printf("VLESS 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("VLESS 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 func() { _ = tc.Close() }()
stream, err := s.OpenStream()
if err != nil {
log.Printf("smux open stream error: %s", err)
return
}
defer func() { _ = stream.Close() }()
pipe(ctx, tc, stream)
}(tcpConn, sess)
}
}
// 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():
return
default:
}
smuxSess, cleanup, err := createSmuxSession(ctx, tp, peer, id)
if err != nil {
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
case <-time.After(2 * time.Second):
}
}
}
// 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, id int) (*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, rawURL, err := tp.getCreds(ctx, tp.link, id)
if err != nil {
return nil, nil, fmt.Errorf("get TURN creds: %w", err)
}
urlhost, urlport, err := net.SplitHostPort(rawURL)
if err != nil {
return nil, nil, 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 nil, nil, fmt.Errorf("resolve TURN addr: %w", err)
}
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 {
c, err1 := net.DialUDP("udp", nil, turnServerUDPAddr)
if err1 != nil {
return nil, nil, fmt.Errorf("dial TURN (udp): %w", err1)
}
cleanupFns = append(cleanupFns, func() { _ = c.Close() })
turnConn = &connectedUDPConn{c}
} else {
var d net.Dialer
c, err1 := d.DialContext(ctx1, "tcp", turnServerAddr)
if err1 != nil {
return nil, nil, fmt.Errorf("dial TURN (tcp): %w", err1)
}
cleanupFns = append(cleanupFns, func() { _ = c.Close() })
turnConn = turn.NewSTUNConn(c)
}
// 3. Create TURN client and allocate 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 {
cleanup()
return nil, nil, fmt.Errorf("create TURN client: %w", err)
}
cleanupFns = append(cleanupFns, func() { turnClient.Close() })
if err = turnClient.Listen(); err != nil {
cleanup()
return nil, nil, fmt.Errorf("TURN listen: %w", err)
}
relayConn, err := turnClient.Allocate()
if err != nil {
cleanup()
return nil, nil, fmt.Errorf("TURN allocate: %w", err)
}
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 {
cleanup()
return nil, nil, fmt.Errorf("generate cert: %w", err)
}
dtlsPC := &relayPacketConn{relay: relayConn, peer: peer}
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)
}
ctx2, cancel2 := context.WithTimeout(ctx, 30*time.Second)
defer cancel2()
if err = dtlsConn.HandshakeContext(ctx2); err != nil {
_ = dtlsConn.Close()
cleanup()
return nil, nil, fmt.Errorf("DTLS handshake: %w", err)
}
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 {
cleanup()
return nil, nil, fmt.Errorf("KCP session: %w", err)
}
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 {
cleanup()
return nil, nil, fmt.Errorf("smux client: %w", err)
}
cleanupFns = append(cleanupFns, func() { _ = smuxSess.Close() })
log.Printf("smux session established")
return smuxSess, cleanup, nil
}
// 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() {
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
wg.Add(2)
go func() {
defer wg.Done()
defer cancel()
if _, err := io.Copy(c1, c2); err != nil {
log.Printf("pipe: c1<-c2 copy error: %v", err)
}
}()
go func() {
defer wg.Done()
defer cancel()
if _, err := io.Copy(c2, c1); err != nil {
log.Printf("pipe: c2<-c1 copy error: %v", err)
}
}()
wg.Wait()
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)
}
}

4
client/manual_captcha.go

@ -541,8 +541,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

7
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"
VLESS_FLAG=""
if [ "${VLESS_MODE}" = "true" ]; then
VLESS_FLAG="-vless"
fi
exec ./vk-turn-proxy -listen 0.0.0.0:56000 -connect "$CONNECT" $VLESS_FLAG

11
go.mod

@ -13,6 +13,8 @@ require (
github.com/pion/logging v0.2.4
github.com/pion/transport/v4 v4.0.1
github.com/pion/turn/v5 v5.0.3
github.com/xtaci/kcp-go/v5 v5.6.18
github.com/xtaci/smux v1.5.34
)
require (
@ -22,13 +24,20 @@ require (
github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect
github.com/bogdanfinn/utls v1.7.7-barnius // indirect
github.com/bogdanfinn/websocket v1.5.5-barnius // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/reedsolomon v1.12.4 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun/v3 v3.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // 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.49.0 // indirect
golang.org/x/mod v0.34.0 // indirect
@ -37,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
)

102
go.sum

@ -1,3 +1,5 @@
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/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
@ -18,10 +20,31 @@ github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45 h1:0b2i5T
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=
@ -30,6 +53,10 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
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.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
@ -44,47 +71,106 @@ 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.3 h1:I+Nw0fQgdPWF1SXDj0egWDhCkcff7gWiigdQpOK52Ak=
github.com/pion/turn/v5 v5.0.3/go.mod h1:fs4SogUh/aRGQzonc4Lx3Jp4EU3j3t0PfNDEd9KcD/w=
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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
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/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
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=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
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.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
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.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
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.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
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.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/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=

2
routes.ps1

@ -1,4 +1,4 @@
# Получаем default gateway (IPv4)
# Получаем default gateway (IPv4)
$gateway = Get-NetRoute `
-DestinationPrefix "0.0.0.0/0" `
| Sort-Object RouteMetric `

301
server/main.go

@ -4,6 +4,7 @@ import (
"context"
"flag"
"fmt"
"io"
"log"
"net"
"os"
@ -12,15 +13,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"
)
const idleTimeout = 2 * time.Minute
func main() {
listen := flag.String("listen", "0.0.0.0:56000", "listen on ip:port")
connect := flag.String("connect", "", "connect to ip:port")
vlessMode := flag.Bool("vless", false, "VLESS mode: forward TCP connections (for VLESS) instead of UDP packets")
flag.Parse()
ctx, cancel := context.WithCancel(context.Background())
@ -45,7 +47,7 @@ func main() {
// Generate a certificate and private key to secure the connection
certificate, genErr := selfsign.GenerateSelfSigned()
if genErr != nil {
panic(err)
panic(genErr)
}
//
@ -94,116 +96,229 @@ 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)
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()
if err := dtlsConn.HandshakeContext(ctx1); err != nil {
log.Printf("Handshake failed: %v", err)
return
}
cancel1()
log.Println("Handshake done")
serverConn, err := net.Dial("udp", *connect)
if err != nil {
log.Println(err)
if *vlessMode {
handleVLESSConnection(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()
}
// handleVLESSConnection creates a KCP+smux session over DTLS and forwards
// each smux stream as a TCP connection to the backend (Xray/VLESS).
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() {
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
smuxSess, err := smux.Server(kcpSess, tcputil.DefaultSmuxConfig())
if err != nil {
log.Printf("smux server error: %s", err)
return
}
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
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 func() {
if err = serverConn.Close(); err != nil {
log.Printf("failed to close outgoing connection: %s", err)
return
if err := s.Close(); err != nil && err != smux.ErrGoAway {
log.Printf("failed to close smux stream: %v", 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(idleTimeout)); 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(idleTimeout)); 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(idleTimeout)); 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(idleTimeout)); err1 != nil {
log.Printf("Failed: %s", err1)
return
}
_, err1 = conn.Write(buf[:n])
if err1 != nil {
log.Printf("Failed: %s", err1)
return
}
// Connect to backend (Xray/VLESS)
backendConn, err := net.DialTimeout("tcp", connectAddr, 10*time.Second)
if err != nil {
log.Printf("backend dial error: %s", err)
return
}
defer func() {
if err := backendConn.Close(); err != nil {
log.Printf("failed to close backend connection: %v", err)
}
}()
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)
defer cancel()
context.AfterFunc(ctx2, func() {
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
wg.Add(2)
go func() {
defer wg.Done()
if _, err := io.Copy(c1, c2); err != nil {
log.Printf("pipeConn: c1<-c2 copy error: %v", err)
}
}()
go func() {
defer wg.Done()
if _, err := io.Copy(c2, c1); err != nil {
log.Printf("pipeConn: c2<-c1 copy error: %v", err)
}
}()
wg.Wait()
// Reset deadlines
_ = c1.SetDeadline(time.Time{})
_ = c2.SetDeadline(time.Time{})
}

103
tcputil/tcputil.go

@ -0,0 +1,103 @@
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, err := kcp.NewNoneBlockCrypt(nil) // DTLS already encrypts
if err != nil {
return nil, err
}
var sess *kcp.UDPSession
if isServer {
// Server: listen on the PacketConn and accept one session
var listener *kcp.Listener
listener, err = kcp.ServeConn(block, 0, 0, pc)
if err != nil {
return nil, err
}
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
}
} 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)
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
}
Loading…
Cancel
Save