refactor: переименовать флаги и поля CLI/конфига

- wrap → obf: -wrap/-wrap-key/-gen-wrap-key → -obf/-obf-key/-gen-obf-key;
  ObfOpts.WrapMode/WrapKey/GenWrapKey → Mode/Key/GenKey
- vless → mode: -vless/-vless-bond → -mode tcp / -bond;
  clientProxyMode/serverProxyMode принимают string вместо bool
- -udp → -transport tcp|udp; TURNOpts.UDP → TransportUDP (везде)
- -vk-link → -link; -dns → -dns-mode; DNSModeUDP → DNSModePlain ("plain")
- KCP: убрать LoadProfileFromEnv/LoadFECFromEnv — хардкодить DefaultProfile/FEC{}
- Добавить валидацию -transport/-mode/-bond/-dns-mode в ParseClient/ParseServer
This commit is contained in:
samosvalishe
2026-05-21 19:19:40 +03:00
parent 6c3d3d341c
commit 31e35efbf3
13 changed files with 166 additions and 212 deletions
+13 -13
View File
@@ -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 {
+9 -9
View File
@@ -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)
+2 -2
View File
@@ -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))
+72 -54
View File
@@ -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
+48 -40
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
+10 -10
View File
@@ -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 {
+2 -2
View File
@@ -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()
+2 -2
View File
@@ -36,8 +36,8 @@ type Params struct {
Host string
Port string
Link string
UDP bool
WrapKey []byte
TransportUDP bool
ObfKey []byte
GetCreds GetCredsFunc
}
-72
View File
@@ -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 {
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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")
}
+3 -3
View File
@@ -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
}