diff --git a/cmd/client/main.go b/cmd/client/main.go index ad96dbc..eb36c06 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -75,10 +75,10 @@ func main() { } appDialer := dnsdial.AppDialer(cfg.DNS.Mode) dnsdial.InstallGlobalResolver(cfg.DNS.Mode) - if cfg.Obf.GenWrapKey { + if cfg.Obf.GenKey { key, gerr := srtpmimicry.GenKeyHex() if gerr != nil { - logger.Errorf("gen-wrap-key: %v", gerr) + logger.Errorf("gen-obf-key: %v", gerr) os.Exit(1) } fmt.Println(key) @@ -89,8 +89,8 @@ func main() { logger.Errorf("resolve peer addr: %v", err) os.Exit(1) } - if cfg.Obf.WrapMode { - logger.Infof("WRAP mode enabled: peer server must use matching -wrap-key") + if cfg.Obf.Mode { + logger.Infof("OBF mode enabled: peer server must use matching -obf-key") } var connectedStreams atomic.Int32 @@ -105,27 +105,27 @@ func main() { }) if cfg.Proxy.Mode != config.ProxyModeUDP { - vlessDtlsDialer := &dtlsdial.Dialer{ + tcpDtlsDialer := &dtlsdial.Dialer{ HandshakeTimeout: 30 * time.Second, HandshakeSem: make(chan struct{}, 3), } bondH := &bondclient.Handler{Deps: bondclient.Deps{Log: logger}} - vlessDeps := &tcpfwd.Deps{ - DTLSDialer: vlessDtlsDialer, + tcpDeps := &tcpfwd.Deps{ + DTLSDialer: tcpDtlsDialer, Log: logger, BondHandler: bondH.Handle, } - vlessParams := &tcpfwd.Params{ + tcpParams := &tcpfwd.Params{ Host: cfg.TURN.Host, Port: cfg.TURN.Port, Link: cfg.VK.Link, - UDP: cfg.TURN.UDP, - WrapKey: cfg.Obf.WrapKey, + TransportUDP: cfg.TURN.TransportUDP, + ObfKey: cfg.Obf.Key, GetCreds: tcpfwd.GetCredsFunc(vkAuth.GetCredentials), KCPProfile: cfg.KCP.Profile, KCPFEC: cfg.KCP.FEC, } - if err := tcpfwd.Run(ctx, vlessDeps, vlessParams, peer, cfg.Proxy.Listen, cfg.TURN.N, cfg.Proxy.Mode == config.ProxyModeTCPFwdBond); err != nil { + if err := tcpfwd.Run(ctx, tcpDeps, tcpParams, peer, cfg.Proxy.Listen, cfg.TURN.N, cfg.Proxy.Mode == config.ProxyModeTCPFwdBond); err != nil { logger.Errorf("tcpfwd: %v", err) os.Exit(1) } @@ -140,8 +140,8 @@ func main() { Host: cfg.TURN.Host, Port: cfg.TURN.Port, Link: cfg.VK.Link, - UDP: cfg.TURN.UDP, - WrapKey: cfg.Obf.WrapKey, + TransportUDP: cfg.TURN.TransportUDP, + ObfKey: cfg.Obf.Key, GetCreds: udprelay.GetCredsFunc(vkAuth.GetCredentials), } if err := udprelay.Run(ctx, udpDtlsDialer, vkAuth, logger, &connectedStreams, udpParams, peer, cfg.Proxy.Listen, cfg.TURN.N); err != nil { diff --git a/cmd/server/main.go b/cmd/server/main.go index e41641a..ad657d9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -29,10 +29,10 @@ func main() { } logger := logx.New(cfg.Log.Debug) - if cfg.Obf.GenWrapKey { + if cfg.Obf.GenKey { key, gerr := srtpmimicry.GenKeyHex() if gerr != nil { - logger.Errorf("gen-wrap-key: %v", gerr) + logger.Errorf("gen-obf-key: %v", gerr) os.Exit(1) } fmt.Println(key) @@ -61,10 +61,10 @@ func main() { logger.Errorf("resolve listen addr: %v", err) os.Exit(1) } - logger.Infof("Starting server listen=%s connect=%s vless=%t wrap=%t bond-autodetect=true", - cfg.Proxy.Listen, cfg.Proxy.Connect, cfg.Proxy.Mode == config.ProxyModeTCPFwd, cfg.Obf.WrapMode) - if !cfg.Obf.WrapMode { - logger.Warnf("running without -wrap: any client reaching %s can relay to %s (no shared-key auth)", cfg.Proxy.Listen, cfg.Proxy.Connect) + logger.Infof("Starting server listen=%s connect=%s mode=%s obf=%t bond-autodetect=true", + cfg.Proxy.Listen, cfg.Proxy.Connect, cfg.Proxy.Mode, cfg.Obf.Mode) + if !cfg.Obf.Mode { + logger.Warnf("running without -obf: any client reaching %s can relay to %s (no shared-key auth)", cfg.Proxy.Listen, cfg.Proxy.Connect) } certificate, genErr := dtlsdial.GenerateSelfSignedCert() @@ -80,9 +80,9 @@ func main() { dtls.WithConnectionIDGenerator(dtls.RandomCIDGenerator(8)), } var listener net.Listener - if cfg.Obf.WrapMode { - logger.Infof("WRAP mode enabled: listener only accepts clients with matching -wrap-key") - wrapListener, werr := srtpmimicry.Listen(addr, cfg.Obf.WrapKey) + if cfg.Obf.Mode { + logger.Infof("OBF mode enabled: listener only accepts clients with matching -obf-key") + wrapListener, werr := srtpmimicry.Listen(addr, cfg.Obf.Key) if werr != nil { logger.Errorf("wrap listen: %v", werr) os.Exit(1) diff --git a/internal/client/dnsdial/doh.go b/internal/client/dnsdial/doh.go index 9509cdc..b2c607b 100644 --- a/internal/client/dnsdial/doh.go +++ b/internal/client/dnsdial/doh.go @@ -364,7 +364,7 @@ func dohForwarderDial(r *DohResolver) dialFunc { } const ( - DNSModeUDP = "udp" + DNSModePlain = "plain" DNSModeDoH = "doh" DNSModeAuto = "auto" ) @@ -417,7 +417,7 @@ type dialFunc = func(context.Context, string, string) (net.Conn, error) // залипает процесс на DoH до конца его жизни. func buildDialer(mode string, r *DohResolver) net.Dialer { switch mode { - case DNSModeUDP: + case DNSModePlain: return newAppDialer(udpDNSDial) case DNSModeDoH: return newAppDialer(dohForwarderDial(r)) diff --git a/internal/config/config.go b/internal/config/config.go index 297e8d9..a91e368 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,7 +20,7 @@ import ( ) const ( - dnsModeUDP = "udp" + dnsModePlain = "plain" dnsModeDoH = "doh" dnsModeAuto = "auto" defaultStreamsPerCache = 10 @@ -32,24 +32,24 @@ const ( type ProxyMode string const ( - ProxyModeUDP ProxyMode = "udp" // -vless=false: UDP-релей пакетов (WireGuard) - ProxyModeTCPFwd ProxyMode = "tcpfwd" // -vless=true: TCP-форвардер через smux - ProxyModeTCPFwdBond ProxyMode = "tcpfwd-bond" // -vless=true -vless-bond=true: bond TCP по N smux-сессиям + ProxyModeUDP ProxyMode = "udp" // -mode udp (default): UDP-релей пакетов (WireGuard) + ProxyModeTCPFwd ProxyMode = "tcpfwd" // -mode tcp: TCP-форвардер через smux + ProxyModeTCPFwdBond ProxyMode = "tcpfwd-bond" // -mode tcp -bond: bond TCP по N smux-сессиям ) // TURNOpts — опции TURN-сервера (куда и как подключаться). type TURNOpts struct { - Host string // -turn: переопределить IP/host TURN-сервера - Port string // -port: переопределить порт TURN - UDP bool // -udp: подключение к TURN по UDP (по умолчанию TCP/TLS) - N int // -n: число TURN-потоков (только клиент) + Host string // -turn: переопределить IP/host TURN-сервера + Port string // -port: переопределить порт TURN + TransportUDP bool // -transport udp: подключение к TURN по UDP (по умолчанию TCP/TLS) + N int // -n: число TURN-потоков (только клиент) } -// ObfOpts — опции обфускации TURN-payload (SRTP-mimicry wrap). +// ObfOpts — опции обфускации TURN-payload (SRTP-mimicry). type ObfOpts struct { - WrapMode bool // -wrap: включить SRTP-mimicry AEAD-обёртку - WrapKey []byte // -wrap-key (декодированный): 32-байтовый общий ключ; nil если WrapMode=false - GenWrapKey bool // -gen-wrap-key: напечатать новый ключ и выйти + Mode bool // -obf: включить SRTP-mimicry AEAD-обёртку + Key []byte // -obf-key (декодированный): 32-байтовый общий ключ; nil если Mode=false + GenKey bool // -gen-obf-key: напечатать новый ключ и выйти } // ProxyOpts — опции прокси прикладного уровня. @@ -62,14 +62,14 @@ type ProxyOpts struct { // VKOpts — опции VK-учёток и captcha (только клиент). type VKOpts struct { - Link string // -vk-link (нормализован до join-кода) + Link string // -link (нормализован до join-кода) StreamsPerCred int // -streams-per-cred ManualCaptcha bool // -manual-captcha } // DNSOpts — опции DNS-резолвинга (только клиент). type DNSOpts struct { - Mode string // -dns: udp | doh | auto + Mode string // -dns-mode: plain | doh | auto Servers []string // -dns-servers (через запятую); nil если флаг пуст } @@ -78,8 +78,7 @@ type LogOpts struct { Debug bool // -debug } -// KCPOpts — опции KCP-туннеля. Обе стороны должны согласовать Profile и FEC; -// значения берутся из переменных окружения VK_TURN_KCP_*. +// KCPOpts — параметры KCP-туннеля, хардкодятся из DefaultProfile/FEC{}. type KCPOpts struct { Profile kcptun.Profile FEC kcptun.FEC @@ -115,19 +114,19 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) { host := fs.String("turn", "", "переопределить IP TURN-сервера (по умолчанию берётся из VK Calls ссылки)") port := fs.String("port", "", "переопределить порт TURN-сервера (по умолчанию берётся из VK Calls ссылки)") listen := fs.String("listen", "127.0.0.1:9000", "локальный адрес ip:port, куда подключается WireGuard или Xray клиент") - vklink := fs.String("vk-link", "", "ссылка VK Calls вида https://vk.com/call/join/... (обязательно)") + vklink := fs.String("link", "", "ссылка VK Calls вида https://vk.com/call/join/... (обязательно)") peerAddr := fs.String("peer", "", "адрес сервера VK TURN Proxy на VPS, host:port (обязательно)") n := fs.Int("n", 10, "количество параллельных TURN-потоков (соединений к TURN-реле)") - udp := fs.Bool("udp", false, "подключаться к TURN-реле по UDP (по умолчанию TCP/TLS)") - vlessMode := fs.Bool("vless", false, "режим TCP-форвардера (VLESS/Xray) вместо UDP-релея для WireGuard") - vlessBond := fs.Bool("vless-bond", false, "распределять одно VLESS TCP-соединение по всем активным smux-сессиям (только с -vless)") - wrapMode := fs.Bool("wrap", false, "маскировать TURN-payload под SRTP (RTP/opus + ChaCha20-Poly1305 AEAD) для обхода content-filter VK; ключ должен совпадать на клиенте и сервере") - wrapKeyHex := fs.String("wrap-key", "", "общий ключ для -wrap, 32 байта в hex (64 символа)") - genWrapKey := fs.Bool("gen-wrap-key", false, "напечатать новый ключ для -wrap-key и выйти") + transportFlag := fs.String("transport", "tcp", "транспорт до TURN-реле: tcp (TCP/TLS, default) | udp") + modeFlag := fs.String("mode", "udp", "режим туннеля: udp (UDP-релей для WireGuard, default) | tcp (TCP-форвардер для Xray/sing-box)") + bondFlag := fs.Bool("bond", false, "распределять одно TCP-соединение по всем активным smux-сессиям (только с -mode tcp)") + wrapMode := fs.Bool("obf", false, "маскировать TURN-payload под SRTP (RTP/opus + ChaCha20-Poly1305 AEAD) для обхода content-filter VK; ключ должен совпадать на клиенте и сервере") + wrapKeyHex := fs.String("obf-key", "", "общий ключ для -obf, 32 байта в hex (64 символа)") + genWrapKey := fs.Bool("gen-obf-key", false, "напечатать новый ключ для -obf-key и выйти") streamsPerCredFlag := fs.Int("streams-per-cred", defaultStreamsPerCache, "сколько TURN-потоков делят один кеш VK-учёток") debugFlag := fs.Bool("debug", false, "включить подробные debug-логи") manualCaptchaFlag := fs.Bool("manual-captcha", false, "пропустить авто-решение VK captcha и сразу открыть ручной режим в локальном браузере") - dnsFlag := fs.String("dns", dnsModeAuto, "режим DNS-резолвинга: udp | doh | auto (auto: сначала UDP/53, sticky-fallback на DoH при полном отказе)") + dnsFlag := fs.String("dns-mode", dnsModeAuto, "транспорт резолвера клиента: plain (UDP/53) | doh (DNS-over-HTTPS) | auto (UDP/53 → sticky DoH при отказе)") dnsServersFlag := fs.String("dns-servers", "", "список UDP/53 DNS-серверов через запятую вместо встроенных (напр. резолверы оператора из Android LinkProperties). Формат: ip[:port][,ip[:port]...]") if err := fs.Parse(args); err != nil { @@ -138,15 +137,15 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) { TURN: TURNOpts{ Host: *host, Port: *port, - UDP: *udp, + TransportUDP: *transportFlag == "udp", N: *n, }, Obf: ObfOpts{ - WrapMode: *wrapMode, - GenWrapKey: *genWrapKey, + Mode: *wrapMode, + GenKey: *genWrapKey, }, Proxy: ProxyOpts{ - Mode: clientProxyMode(*vlessMode, *vlessBond), + Mode: clientProxyMode(*modeFlag, *bondFlag), Listen: *listen, Peer: *peerAddr, }, @@ -161,21 +160,34 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) { Debug: *debugFlag, }, KCP: KCPOpts{ - Profile: kcptun.LoadProfileFromEnv(), - FEC: kcptun.LoadFECFromEnv(), + Profile: kcptun.DefaultProfile(), + FEC: kcptun.FEC{}, }, } - switch c.DNS.Mode { - case dnsModeUDP, dnsModeDoH, dnsModeAuto: + switch *transportFlag { + case "tcp", "udp": default: - return nil, fmt.Errorf("invalid -dns value %q: must be udp | doh | auto", c.DNS.Mode) + return nil, fmt.Errorf("invalid -transport value %q: must be tcp | udp", *transportFlag) + } + switch *modeFlag { + case "udp", "tcp": + default: + return nil, fmt.Errorf("invalid -mode value %q: must be udp | tcp", *modeFlag) + } + if *bondFlag && *modeFlag != "tcp" { + return nil, fmt.Errorf("-bond requires -mode tcp") + } + switch c.DNS.Mode { + case dnsModePlain, dnsModeDoH, dnsModeAuto: + default: + return nil, fmt.Errorf("invalid -dns-mode value %q: must be plain | doh | auto", c.DNS.Mode) } if *dnsServersFlag != "" { c.DNS.Servers = strings.Split(*dnsServersFlag, ",") } - if c.Obf.GenWrapKey { + if c.Obf.GenKey { return c, nil } @@ -183,13 +195,13 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) { return nil, errors.New("need peer address") } if *vklink == "" { - return nil, errors.New("need vk-link") + return nil, errors.New("need -link") } - key, err := srtpmimicry.DecodeKey(c.Obf.WrapMode, *wrapKeyHex) + key, err := srtpmimicry.DecodeKey(c.Obf.Mode, *wrapKeyHex) if err != nil { return nil, err } - c.Obf.WrapKey = key + c.Obf.Key = key if c.VK.StreamsPerCred <= 0 { return nil, fmt.Errorf("-streams-per-cred must be positive") } @@ -216,10 +228,10 @@ func ParseServer(args []string, errOut io.Writer) (*Server, error) { listen := fs.String("listen", "0.0.0.0:56000", "локальный адрес прослушивания ip:port") connect := fs.String("connect", "", "адрес локального бэкенда, host:port (обязательно: WireGuard 127.0.0.1:51820 или Xray 127.0.0.1:443)") - vlessMode := fs.Bool("vless", false, "режим TCP-форвардера (VLESS/Xray) вместо UDP-релея для WireGuard; bond определяется автоматически по magic-префиксу в стриме") - wrapMode := fs.Bool("wrap", false, "маскировать TURN-payload под SRTP (RTP/opus + ChaCha20-Poly1305 AEAD) для обхода content-filter VK; ключ должен совпадать с клиентом") - wrapKeyHex := fs.String("wrap-key", "", "общий ключ для -wrap, 32 байта в hex (64 символа)") - genWrapKey := fs.Bool("gen-wrap-key", false, "напечатать новый ключ для -wrap-key и выйти") + modeFlag := fs.String("mode", "udp", "режим туннеля: udp (UDP-релей для WireGuard, default) | tcp (TCP-форвардер для Xray/sing-box; bond определяется автоматически)") + wrapMode := fs.Bool("obf", false, "маскировать TURN-payload под SRTP (RTP/opus + ChaCha20-Poly1305 AEAD) для обхода content-filter VK; ключ должен совпадать с клиентом") + wrapKeyHex := fs.String("obf-key", "", "общий ключ для -obf, 32 байта в hex (64 символа)") + genWrapKey := fs.Bool("gen-obf-key", false, "напечатать новый ключ для -obf-key и выйти") debugFlag := fs.Bool("debug", false, "включить подробные debug-логи") if err := fs.Parse(args); err != nil { @@ -228,11 +240,11 @@ func ParseServer(args []string, errOut io.Writer) (*Server, error) { s := &Server{ Obf: ObfOpts{ - WrapMode: *wrapMode, - GenWrapKey: *genWrapKey, + Mode: *wrapMode, + GenKey: *genWrapKey, }, Proxy: ProxyOpts{ - Mode: serverProxyMode(*vlessMode), + Mode: serverProxyMode(*modeFlag), Listen: *listen, Connect: *connect, }, @@ -240,40 +252,46 @@ func ParseServer(args []string, errOut io.Writer) (*Server, error) { Debug: *debugFlag, }, KCP: KCPOpts{ - Profile: kcptun.LoadProfileFromEnv(), - FEC: kcptun.LoadFECFromEnv(), + Profile: kcptun.DefaultProfile(), + FEC: kcptun.FEC{}, }, } - if s.Obf.GenWrapKey { + switch *modeFlag { + case "udp", "tcp": + default: + return nil, fmt.Errorf("invalid -mode value %q: must be udp | tcp", *modeFlag) + } + + if s.Obf.GenKey { return s, nil } if s.Proxy.Connect == "" { return nil, fmt.Errorf("server address is required") } - key, err := srtpmimicry.DecodeKey(s.Obf.WrapMode, *wrapKeyHex) + key, err := srtpmimicry.DecodeKey(s.Obf.Mode, *wrapKeyHex) if err != nil { return nil, err } - s.Obf.WrapKey = key + s.Obf.Key = key return s, nil } -func clientProxyMode(vless, bond bool) ProxyMode { +func clientProxyMode(mode string, bond bool) ProxyMode { switch { - case vless && bond: + case mode == "tcp" && bond: return ProxyModeTCPFwdBond - case vless: + case mode == "tcp": return ProxyModeTCPFwd default: return ProxyModeUDP } } -func serverProxyMode(vless bool) ProxyMode { - if vless { +func serverProxyMode(mode string) ProxyMode { + if mode == "tcp" { return ProxyModeTCPFwd } return ProxyModeUDP diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 40ff107..b9dbf92 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -12,7 +12,7 @@ import ( func validClientArgs() []string { return []string{ "-peer", "1.2.3.4:5000", - "-vk-link", "https://vk.com/call/join/abcdef", + "-link", "https://vk.com/call/join/abcdef", } } @@ -36,8 +36,8 @@ func TestParseClient_Defaults(t *testing.T) { if c.VK.Link != "abcdef" { t.Errorf("VK.Link: %q (expected abcdef)", c.VK.Link) } - if c.Obf.WrapKey != nil { - t.Errorf("Obf.WrapKey should be nil when -wrap absent") + if c.Obf.Key != nil { + t.Errorf("Obf.Key should be nil when -obf absent") } if c.Proxy.Mode != ProxyModeUDP { t.Errorf("Proxy.Mode default: %q (expected udp)", c.Proxy.Mode) @@ -47,7 +47,7 @@ func TestParseClient_Defaults(t *testing.T) { func TestParseClient_VKLinkStrip(t *testing.T) { args := []string{ "-peer", "1.2.3.4:5000", - "-vk-link", "https://vk.com/call/join/CODE123?foo=bar", + "-link", "https://vk.com/call/join/CODE123?foo=bar", } c, err := ParseClient(args, io.Discard) if err != nil { @@ -59,7 +59,7 @@ func TestParseClient_VKLinkStrip(t *testing.T) { } func TestParseClient_MissingPeer(t *testing.T) { - _, err := ParseClient([]string{"-vk-link", "https://vk.com/call/join/X"}, io.Discard) + _, err := ParseClient([]string{"-link", "https://vk.com/call/join/X"}, io.Discard) if err == nil || !strings.Contains(err.Error(), "peer") { t.Errorf("expected peer error, got %v", err) } @@ -67,35 +67,43 @@ func TestParseClient_MissingPeer(t *testing.T) { func TestParseClient_MissingVKLink(t *testing.T) { _, err := ParseClient([]string{"-peer", "1.2.3.4:5000"}, io.Discard) - if err == nil || !strings.Contains(err.Error(), "vk-link") { + if err == nil || !strings.Contains(err.Error(), "-link") { t.Errorf("expected vk-link error, got %v", err) } } func TestParseClient_InvalidDNS(t *testing.T) { - args := append(validClientArgs(), "-dns", "garbage") + args := append(validClientArgs(), "-dns-mode", "garbage") _, err := ParseClient(args, io.Discard) - if err == nil || !strings.Contains(err.Error(), "invalid -dns") { + if err == nil || !strings.Contains(err.Error(), "invalid -dns-mode") { t.Errorf("expected dns error, got %v", err) } } -func TestParseClient_WrapMissingKey(t *testing.T) { - args := append(validClientArgs(), "-wrap") +func TestParseClient_BondWithoutTCPMode(t *testing.T) { + args := append(validClientArgs(), "-bond") _, err := ParseClient(args, io.Discard) - if err == nil || !strings.Contains(err.Error(), "-wrap requires -wrap-key") { - t.Errorf("expected wrap-key error, got %v", err) + if err == nil || !strings.Contains(err.Error(), "-bond requires -mode tcp") { + t.Errorf("expected bond error, got %v", err) } } -func TestParseClient_WrapKeyOK(t *testing.T) { - args := append(validClientArgs(), "-wrap", "-wrap-key", strings.Repeat("ab", 32)) +func TestParseClient_ObfMissingKey(t *testing.T) { + args := append(validClientArgs(), "-obf") + _, err := ParseClient(args, io.Discard) + if err == nil || !strings.Contains(err.Error(), "-obf requires -obf-key") { + t.Errorf("expected obf-key error, got %v", err) + } +} + +func TestParseClient_ObfKeyOK(t *testing.T) { + args := append(validClientArgs(), "-obf", "-obf-key", strings.Repeat("ab", 32)) c, err := ParseClient(args, io.Discard) if err != nil { t.Fatal(err) } - if len(c.Obf.WrapKey) != 32 { - t.Errorf("Obf.WrapKey len: %d", len(c.Obf.WrapKey)) + if len(c.Obf.Key) != 32 { + t.Errorf("Obf.Key len: %d", len(c.Obf.Key)) } } @@ -129,13 +137,13 @@ func TestParseClient_DNSServersSplit(t *testing.T) { } } -func TestParseClient_GenWrapKeySkipsPeerCheck(t *testing.T) { - c, err := ParseClient([]string{"-gen-wrap-key"}, io.Discard) +func TestParseClient_GenObfKeySkipsPeerCheck(t *testing.T) { + c, err := ParseClient([]string{"-gen-obf-key"}, io.Discard) if err != nil { - t.Fatalf("gen-wrap-key should not require peer/vk-link: %v", err) + t.Fatalf("gen-obf-key should not require peer/vk-link: %v", err) } - if !c.Obf.GenWrapKey { - t.Errorf("Obf.GenWrapKey not set") + if !c.Obf.GenKey { + t.Errorf("Obf.GenKey not set") } } @@ -154,9 +162,9 @@ func TestParseClient_ProxyMode(t *testing.T) { want ProxyMode }{ {"default-udp", nil, ProxyModeUDP}, - {"vless", []string{"-vless"}, ProxyModeTCPFwd}, - {"vless-bond", []string{"-vless", "-vless-bond"}, ProxyModeTCPFwdBond}, - {"bond-without-vless-ignored", []string{"-vless-bond"}, ProxyModeUDP}, + {"tcp", []string{"-mode", "tcp"}, ProxyModeTCPFwd}, + {"tcp-bond", []string{"-mode", "tcp", "-bond"}, ProxyModeTCPFwdBond}, + {"default-udp-no-bond", nil, ProxyModeUDP}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -195,42 +203,42 @@ func TestParseServer_MissingConnect(t *testing.T) { } } -func TestParseServer_WrapMissingKey(t *testing.T) { - _, err := ParseServer([]string{"-connect", "x:1", "-wrap"}, io.Discard) - if err == nil || !strings.Contains(err.Error(), "-wrap requires -wrap-key") { - t.Errorf("expected wrap-key error, got %v", err) +func TestParseServer_ObfMissingKey(t *testing.T) { + _, err := ParseServer([]string{"-connect", "x:1", "-obf"}, io.Discard) + if err == nil || !strings.Contains(err.Error(), "-obf requires -obf-key") { + t.Errorf("expected obf-key error, got %v", err) } } -func TestParseServer_WrapKeyBadHex(t *testing.T) { - _, err := ParseServer([]string{"-connect", "x:1", "-wrap", "-wrap-key", "zz"}, io.Discard) +func TestParseServer_ObfKeyBadHex(t *testing.T) { + _, err := ParseServer([]string{"-connect", "x:1", "-obf", "-obf-key", "zz"}, io.Discard) if err == nil || !strings.Contains(err.Error(), "invalid hex") { t.Errorf("expected hex error, got %v", err) } } -func TestParseServer_WrapKeyOK(t *testing.T) { - s, err := ParseServer([]string{"-connect", "x:1", "-wrap", "-wrap-key", strings.Repeat("cd", 32)}, io.Discard) +func TestParseServer_ObfKeyOK(t *testing.T) { + s, err := ParseServer([]string{"-connect", "x:1", "-obf", "-obf-key", strings.Repeat("cd", 32)}, io.Discard) if err != nil { t.Fatal(err) } - if len(s.Obf.WrapKey) != 32 { - t.Errorf("Obf.WrapKey len: %d", len(s.Obf.WrapKey)) + if len(s.Obf.Key) != 32 { + t.Errorf("Obf.Key len: %d", len(s.Obf.Key)) } } -func TestParseServer_GenWrapKeySkipsConnectCheck(t *testing.T) { - s, err := ParseServer([]string{"-gen-wrap-key"}, io.Discard) +func TestParseServer_GenObfKeySkipsConnectCheck(t *testing.T) { + s, err := ParseServer([]string{"-gen-obf-key"}, io.Discard) if err != nil { - t.Fatalf("gen-wrap-key should not require -connect: %v", err) + t.Fatalf("gen-obf-key should not require -connect: %v", err) } - if !s.Obf.GenWrapKey { - t.Errorf("Obf.GenWrapKey not set") + if !s.Obf.GenKey { + t.Errorf("Obf.GenKey not set") } } func TestParseServer_ProxyMode(t *testing.T) { - s, err := ParseServer([]string{"-connect", "x:1", "-vless"}, io.Discard) + s, err := ParseServer([]string{"-connect", "x:1", "-mode", "tcp"}, io.Discard) if err != nil { t.Fatal(err) } diff --git a/internal/proxy/common/common.go b/internal/proxy/common/common.go index b5da403..dc77f29 100644 --- a/internal/proxy/common/common.go +++ b/internal/proxy/common/common.go @@ -28,7 +28,7 @@ func DialTURN(ctx context.Context, host, port string, udp bool, peer *net.UDPAdd return turndial.Open(ctx, turndial.Config{ HostOverride: host, PortOverride: port, - UDP: udp, + TransportUDP: udp, }, peer, user, pass, rawURL) } diff --git a/internal/proxy/tcpfwd/tcpfwd.go b/internal/proxy/tcpfwd/tcpfwd.go index 8a6a3c7..31f70a3 100644 --- a/internal/proxy/tcpfwd/tcpfwd.go +++ b/internal/proxy/tcpfwd/tcpfwd.go @@ -26,8 +26,8 @@ type Params struct { Host string Port string Link string - UDP bool - WrapKey []byte + TransportUDP bool + ObfKey []byte GetCreds GetCredsFunc KCPProfile kcptun.Profile KCPFEC kcptun.FEC @@ -38,7 +38,7 @@ type Params struct { // Реализация — internal/proxy/bondclient. type BondHandler func(ctx context.Context, tcpConn net.Conn, connID uint64, lanes []*PooledSession) -// Deps — зависимости хост-процесса для VLESS-цикла. +// Deps — зависимости хост-процесса для TCP-forward цикла. type Deps struct { DTLSDialer *dtlsdial.Dialer Log logx.Logger @@ -52,7 +52,7 @@ func (d *Deps) log() logx.Logger { return d.Log } -// Run — точка входа VLESS-режима. Запускает numSessions maintainer-горутин, ждёт +// Run — точка входа TCP-forward режима. Запускает numSessions maintainer-горутин, ждёт // первого подключения, затем принимает локальные TCP-соединения и форвардит // каждое как smux-поток (round-robin) или bonded по всем активным сессиям. func Run(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, listenAddr string, numSessions int, useBond bool) error { @@ -70,7 +70,7 @@ func Run(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, lis }) } - deps.log().Infof("VLESS mode: waiting for sessions to connect (total: %d)...", numSessions) + deps.log().Infof("TCP mode: waiting for sessions to connect (total: %d)...", numSessions) select { case <-ctx.Done(): wgMaint.Wait() @@ -92,9 +92,9 @@ func Run(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, lis context.AfterFunc(ctx, func() { _ = wrappedListener.Close() }) if useBond { - deps.log().Infof("VLESS bond mode: listening on %s (striping each TCP connection across active sessions)", listenAddr) + deps.log().Infof("TCP bond mode: listening on %s (striping each TCP connection across active sessions)", listenAddr) } else { - deps.log().Infof("VLESS mode: listening on %s (round-robin across %d sessions)", listenAddr, numSessions) + deps.log().Infof("TCP mode: listening on %s (round-robin across %d sessions)", listenAddr, numSessions) } var wgConn sync.WaitGroup @@ -232,7 +232,7 @@ func createSmuxSession(ctx context.Context, deps *Deps, params *Params, peer *ne } } - stream, err := common.DialTURN(ctx, params.Host, params.Port, params.UDP, peer, params.Link, id, params.GetCreds) + stream, err := common.DialTURN(ctx, params.Host, params.Port, params.TransportUDP, peer, params.Link, id, params.GetCreds) if err != nil { return nil, nil, err } @@ -241,7 +241,7 @@ func createSmuxSession(ctx context.Context, deps *Deps, params *Params, peer *ne deps.log().Debugf("[session %d] TURN server IP: %s", id, stream.ServerUDPAddr.IP) deps.log().Debugf("relayed-address=%s", relayConn.LocalAddr().String()) - relayWC, err := common.NewClientWrap(params.WrapKey) + relayWC, err := common.NewClientWrap(params.ObfKey) if err != nil { cleanup() return nil, nil, fmt.Errorf("wrap init: %w", err) @@ -258,7 +258,7 @@ func createSmuxSession(ctx context.Context, deps *Deps, params *Params, peer *ne statsCtx, statsCancel := context.WithCancel(ctx) cleanupFns = append(cleanupFns, statsCancel) st := stats.New(deps.log().DebugEnabled()) - go st.LogEvery(statsCtx, deps.log().Debugf, fmt.Sprintf("[session %d] VLESS", id), "to-turn", "from-turn") + go st.LogEvery(statsCtx, deps.log().Debugf, fmt.Sprintf("[session %d] TCP", id), "to-turn", "from-turn") kcpSess, err := kcptun.NewKCPOverDTLS(&stats.CountingConn{Conn: dtlsConn, Stats: st}, false, params.KCPProfile, params.KCPFEC) if err != nil { diff --git a/internal/proxy/udprelay/loop.go b/internal/proxy/udprelay/loop.go index 4624495..8e86f02 100644 --- a/internal/proxy/udprelay/loop.go +++ b/internal/proxy/udprelay/loop.go @@ -218,7 +218,7 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, err = ctx.Err() return } - stream, err1 := common.DialTURN(ctx, params.Host, params.Port, params.UDP, peer, params.Link, streamID, params.GetCreds) + stream, err1 := common.DialTURN(ctx, params.Host, params.Port, params.TransportUDP, peer, params.Link, streamID, params.GetCreds) if err1 != nil { if deps.Auth.IsAuthError(err1) { deps.Auth.HandleAuthError(streamID) @@ -254,7 +254,7 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, } }) var internalPipeAddr atomic.Value - wc, wcErr := common.NewClientWrap(params.WrapKey) + wc, wcErr := common.NewClientWrap(params.ObfKey) if wcErr != nil { deps.log().Errorf("[STREAM %d] WRAP init failed: %v", streamID, wcErr) turncancel() diff --git a/internal/proxy/udprelay/run.go b/internal/proxy/udprelay/run.go index 546387c..4239ff2 100644 --- a/internal/proxy/udprelay/run.go +++ b/internal/proxy/udprelay/run.go @@ -36,8 +36,8 @@ type Params struct { Host string Port string Link string - UDP bool - WrapKey []byte + TransportUDP bool + ObfKey []byte GetCreds GetCredsFunc } diff --git a/internal/transport/kcptun/kcptun.go b/internal/transport/kcptun/kcptun.go index a44fa54..179cf64 100644 --- a/internal/transport/kcptun/kcptun.go +++ b/internal/transport/kcptun/kcptun.go @@ -2,9 +2,6 @@ package kcptun import ( "net" - "os" - "strconv" - "strings" "time" "github.com/xtaci/kcp-go/v5" @@ -43,75 +40,6 @@ func DefaultProfile() Profile { } } -// LoadProfileFromEnv читает VK_TURN_KCP_PROFILE и per-field переопределения -// VK_TURN_KCP_*. Неизвестное имя профиля → DefaultProfile. -func LoadProfileFromEnv() Profile { - name := strings.ToLower(strings.TrimSpace(os.Getenv("VK_TURN_KCP_PROFILE"))) - var p Profile - switch name { - case "legacy", "fast": - p = Profile{NoDelay: 1, Interval: 10, Resend: 2, NC: 1, SndWnd: 4096, RcvWnd: 4096, MTU: 1280, ACKNoDelay: true} - case "cc", "balanced": - p = Profile{NoDelay: 1, Interval: 20, Resend: 2, NC: 0, SndWnd: 512, RcvWnd: 512, MTU: 1200, ACKNoDelay: true} - case "slow", "conservative": - p = Profile{NoDelay: 0, Interval: 40, Resend: 2, NC: 0, SndWnd: 256, RcvWnd: 256, MTU: 1150, ACKNoDelay: false} - default: - p = DefaultProfile() - } - p.NoDelay = envInt("VK_TURN_KCP_NODELAY", p.NoDelay) - p.Interval = envInt("VK_TURN_KCP_INTERVAL", p.Interval) - p.Resend = envInt("VK_TURN_KCP_RESEND", p.Resend) - p.NC = envInt("VK_TURN_KCP_NC", p.NC) - p.SndWnd = envInt("VK_TURN_KCP_SNDWND", p.SndWnd) - p.RcvWnd = envInt("VK_TURN_KCP_RCVWND", p.RcvWnd) - p.MTU = envInt("VK_TURN_KCP_MTU", p.MTU) - p.ACKNoDelay = envBool("VK_TURN_KCP_ACK_NODELAY", p.ACKNoDelay) - return p -} - -// LoadFECFromEnv парсит VK_TURN_KCP_FEC как "data:parity" (напр. "10:3"). -// Пустое/невалидное → отключено. -func LoadFECFromEnv() FEC { - raw := strings.TrimSpace(os.Getenv("VK_TURN_KCP_FEC")) - if raw == "" { - return FEC{} - } - parts := strings.SplitN(raw, ":", 2) - if len(parts) != 2 { - return FEC{} - } - d, err1 := strconv.Atoi(strings.TrimSpace(parts[0])) - p, err2 := strconv.Atoi(strings.TrimSpace(parts[1])) - if err1 != nil || err2 != nil || d <= 0 || p <= 0 { - return FEC{} - } - return FEC{Data: d, Parity: p} -} - -func envInt(name string, fallback int) int { - raw := strings.TrimSpace(os.Getenv(name)) - if raw == "" { - return fallback - } - value, err := strconv.Atoi(raw) - if err != nil { - return fallback - } - return value -} - -func envBool(name string, fallback bool) bool { - raw := strings.ToLower(strings.TrimSpace(os.Getenv(name))) - switch raw { - case "1", "true", "yes", "on": - return true - case "0", "false", "no", "off": - return false - default: - return fallback - } -} - // DtlsPacketConn оборачивает net.Conn (DTLS) как net.PacketConn для KCP. // Каждый DTLS Read/Write сохраняет границы сообщений (datagram семантика). type DtlsPacketConn struct { diff --git a/internal/transport/turndial/turndial.go b/internal/transport/turndial/turndial.go index 4a07b8a..f871487 100644 --- a/internal/transport/turndial/turndial.go +++ b/internal/transport/turndial/turndial.go @@ -24,8 +24,8 @@ type Config struct { HostOverride string // PortOverride, если непустой, заменяет port из lookup credentials. PortOverride string - // UDP=true — dial TURN по UDP; иначе по TCP через STUNConn. - UDP bool + // TransportUDP=true — dial TURN по UDP; иначе по TCP через STUNConn. + TransportUDP bool // DialTimeout ограничивает TCP dial. Ноль → 5s. DialTimeout time.Duration } @@ -77,7 +77,7 @@ func Open(ctx context.Context, cfg Config, peer *net.UDPAddr, user, pass, rawAdd turnConn net.PacketConn closeConn func() error ) - if cfg.UDP { + if cfg.TransportUDP { c, derr := net.DialUDP("udp", nil, turnServerUDPAddr) //nolint:noctx if derr != nil { return nil, fmt.Errorf("dial TURN (udp): %w", derr) diff --git a/internal/transport/turndial/turndial_test.go b/internal/transport/turndial/turndial_test.go index b106604..017fde1 100644 --- a/internal/transport/turndial/turndial_test.go +++ b/internal/transport/turndial/turndial_test.go @@ -26,7 +26,7 @@ func TestOpen_HostOverrideApplied(t *testing.T) { peer := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1} ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - _, err := Open(ctx, Config{HostOverride: "127.0.0.1", PortOverride: "1", UDP: false, DialTimeout: 200 * time.Millisecond}, peer, "u", "p", "8.8.8.8:443") + _, err := Open(ctx, Config{HostOverride: "127.0.0.1", PortOverride: "1", TransportUDP: false,DialTimeout: 200 * time.Millisecond}, peer, "u", "p", "8.8.8.8:443") if err == nil { t.Fatal("expected dial error against unreachable :1") } diff --git a/internal/wire/srtpmimicry/srtpmimicry.go b/internal/wire/srtpmimicry/srtpmimicry.go index 0311764..63e1188 100644 --- a/internal/wire/srtpmimicry/srtpmimicry.go +++ b/internal/wire/srtpmimicry/srtpmimicry.go @@ -190,14 +190,14 @@ func DecodeKey(enabled bool, raw string) ([]byte, error) { return nil, nil } if raw == "" { - return nil, errors.New("-wrap requires -wrap-key") + return nil, errors.New("-obf requires -obf-key") } key, err := hex.DecodeString(raw) if err != nil { - return nil, fmt.Errorf("-wrap-key invalid hex: %w", err) + return nil, fmt.Errorf("-obf-key invalid hex: %w", err) } if len(key) != KeyLen { - return nil, fmt.Errorf("-wrap-key must decode to %d bytes (got %d)", KeyLen, len(key)) + return nil, fmt.Errorf("-obf-key must decode to %d bytes (got %d)", KeyLen, len(key)) } return key, nil }