From 817def3e0165369e5deea08a4e875c0627d14e90 Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Tue, 19 May 2026 16:26:36 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20CLI=20?= =?UTF-8?q?=D0=B2=20internal/config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ParseClient/ParseServer возвращают валидированные структуры; main становится связкой. Валидация без сайд-эффектов (без DNS/сети); main держит ResolveUDPAddr, установки dnsdial.* и вывод gen-wrap-key. Сервер теперь переиспользует wrap.GenKeyHex/wrap.DecodeKey вместо своего hex+rand пути. client/main.go: 319 → 272. server/main.go: 424 → 402. 19 тестов покрывают дефолты, санитизацию vk-link, отсутствующие флаги, валидацию -dns, конфликт wrap+no-dtls, декод wrap-key, положительность streams-per-cred, clamp n, split dns-servers и short-circuit gen-wrap-key. Флаги CLI, дефолты и поведение не изменены. Две строки ошибок ("Need peer address!" / "Need vk-link!") переведены в нижний регистр под revive; не часть wire-контракта. --- client/main.go | 105 +++++------------ internal/config/config.go | 200 +++++++++++++++++++++++++++++++ internal/config/config_test.go | 207 +++++++++++++++++++++++++++++++++ server/main.go | 58 +++------ 4 files changed, 454 insertions(+), 116 deletions(-) create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go diff --git a/client/main.go b/client/main.go index 6f031b6..8d6b341 100644 --- a/client/main.go +++ b/client/main.go @@ -5,13 +5,11 @@ package main import ( "context" - "flag" "fmt" "log" "net" "os" "os/signal" - "strings" "sync" "sync/atomic" "syscall" @@ -21,6 +19,7 @@ import ( "github.com/cacggghp/vk-turn-proxy/client/internal/dnsdial" "github.com/cacggghp/vk-turn-proxy/client/internal/vkauth" bondclient "github.com/cacggghp/vk-turn-proxy/internal/bond/client" + "github.com/cacggghp/vk-turn-proxy/internal/config" "github.com/cacggghp/vk-turn-proxy/internal/dtlsdial" udpproxy "github.com/cacggghp/vk-turn-proxy/internal/proxy/udp" "github.com/cacggghp/vk-turn-proxy/internal/proxy/vless" @@ -91,102 +90,56 @@ func main() { log.Fatalf("Exit...\n") }() - host := flag.String("turn", "", "override TURN server ip") - port := flag.String("port", "", "override TURN port") - listen := flag.String("listen", "127.0.0.1:9000", "listen on ip:port") - vklink := flag.String("vk-link", "", "VK calls invite link \"https://vk.com/call/join/...\"") - peerAddr := flag.String("peer", "", "peer server address (host:port)") - n := flag.Int("n", 10, "connections to TURN") - 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") - vlessBond := flag.Bool("vless-bond", false, "bond one VLESS TCP connection across all active smux sessions") - wrapMode := flag.Bool("wrap", false, "WRAP mode: ChaCha20-XOR obfuscate DTLS packets before they reach TURN ChannelData") - wrapKeyHex := flag.String("wrap-key", "", "32-byte hex-encoded shared key for -wrap (64 hex chars)") - genWrapKey := flag.Bool("gen-wrap-key", false, "print a fresh 64-character hex key for -wrap-key and exit") - streamsPerCredFlag := flag.Int("streams-per-cred", vkauth.DefaultStreamsPerCache, "number of TURN streams sharing one VK credential cache") - debugFlag := flag.Bool("debug", false, "enable debug logging") - manualCaptchaFlag := flag.Bool("manual-captcha", false, "skip auto captcha solving, use manual mode immediately") - dnsFlag := flag.String("dns", dnsdial.DNSModeAuto, "DNS resolution mode: udp | doh | auto (auto tries UDP/53 first, sticky-fallback to DoH on total failure)") - dnsServersFlag := flag.String("dns-servers", "", "comma-separated UDP/53 DNS servers to use instead of built-in defaults (e.g. carrier resolvers from Android LinkProperties). Format: ip[:port][,ip[:port]...].") - flag.Parse() + cfg, err := config.ParseClient(os.Args[1:], os.Stderr) + if err != nil { + log.Panicf("%v", err) + } - switch *dnsFlag { - case dnsdial.DNSModeUDP, dnsdial.DNSModeDoH, dnsdial.DNSModeAuto: - default: - log.Panicf("invalid -dns value %q: must be udp | doh | auto", *dnsFlag) + if cfg.DNSServers != nil { + dnsdial.SetUDPDNSServers(cfg.DNSServers) + log.Printf("[DNS] using custom UDP servers: %v", cfg.DNSServers) } - if *dnsServersFlag != "" { - servers := strings.Split(*dnsServersFlag, ",") - dnsdial.SetUDPDNSServers(servers) - log.Printf("[DNS] using custom UDP servers: %v", servers) - } - appDialer = dnsdial.AppDialer(*dnsFlag) - dnsdial.InstallGlobalResolver(*dnsFlag) - if *genWrapKey { - key, err := wrap.GenKeyHex() - if err != nil { - log.Panicf("%v", err) + appDialer = dnsdial.AppDialer(cfg.DNSMode) + dnsdial.InstallGlobalResolver(cfg.DNSMode) + if cfg.GenWrapKey { + key, gerr := wrap.GenKeyHex() + if gerr != nil { + log.Panicf("%v", gerr) } fmt.Println(key) return } - if *peerAddr == "" { - log.Panicf("Need peer address!") - } - peer, err := net.ResolveUDPAddr("udp", *peerAddr) + peer, err := net.ResolveUDPAddr("udp", cfg.Peer) if err != nil { panic(err) } - if *vklink == "" { - log.Panicf("Need vk-link!") - } - if *wrapMode && *direct { - log.Panicf("-wrap requires DTLS; remove -no-dtls") - } - wrapKey, err := wrap.DecodeKey(*wrapMode, *wrapKeyHex) - if err != nil { - log.Panicf("%v", err) - } - if *wrapMode { + if cfg.WrapMode { log.Printf("WRAP mode enabled: peer server must use matching -wrap-key") } - if *streamsPerCredFlag <= 0 { - log.Panicf("-streams-per-cred must be positive") - } - isDebug = *debugFlag + isDebug = cfg.Debug vkAuth = vkauth.New(vkauth.Config{ Dialer: appDialer, - ManualOnly: *manualCaptchaFlag, - StreamsPerCache: *streamsPerCredFlag, + ManualOnly: cfg.ManualCaptcha, + StreamsPerCache: cfg.StreamsPerCred, StreamsAlive: func() int32 { return connectedStreams.Load() }, ManualSolver: manualCaptchaSolver, Debugf: debugf, }) - parts := strings.Split(*vklink, "join/") - link := parts[len(parts)-1] - getCreds := getCredsFunc(vkAuth.GetCredentials) - if *n <= 0 { - *n = 10 - } - if idx := strings.IndexAny(link, "/?#"); idx != -1 { - link = link[:idx] - } params := &turnParams{ - host: *host, - port: *port, - link: link, - udp: *udp, - wrapKey: wrapKey, + host: cfg.Host, + port: cfg.Port, + link: cfg.VKLink, + udp: cfg.UDP, + wrapKey: cfg.WrapKey, getCreds: getCreds, } - if *vlessMode { + if cfg.VLESSMode { bondH := &bondclient.Handler{Deps: bondclient.Deps{Debug: isDebug, Debugf: debugf}} vlessDeps := &vless.Deps{ DTLSDialer: vlessDtlsDialer, @@ -202,11 +155,11 @@ func main() { WrapKey: params.wrapKey, GetCreds: vless.GetCredsFunc(params.getCreds), } - vless.Run(ctx, vlessDeps, vlessParams, peer, *listen, *n, *vlessBond) + vless.Run(ctx, vlessDeps, vlessParams, peer, cfg.Listen, cfg.N, cfg.VLESSBond) return } - listenConn, err := net.ListenPacket("udp", *listen) + listenConn, err := net.ListenPacket("udp", cfg.Listen) if err != nil { log.Panicf("Failed to listen: %s", err) } @@ -216,7 +169,7 @@ func main() { } }) - numStreams := *n + numStreams := cfg.N if numStreams <= 0 { numStreams = 1 } @@ -268,7 +221,7 @@ func main() { wg1 := sync.WaitGroup{} t := time.Tick(200 * time.Millisecond) - if *direct { + if cfg.Direct { log.Panicf("Direct mode not supported with dispatcher") } diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..45967da --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,200 @@ +// Package config parses CLI flags for the client and server binaries. +// +// Parse* functions are side-effect free: they validate inputs and decode the +// wrap key, but do not touch the network, DNS, or process state. main() is +// responsible for wiring those side effects after Parse* returns. +package config + +import ( + "errors" + "flag" + "fmt" + "io" + "strings" + + "github.com/cacggghp/vk-turn-proxy/internal/wrap" +) + +// Mirrors of constants defined in client/internal packages, duplicated here +// because internal/config cannot import client/internal/*. +const ( + dnsModeUDP = "udp" + dnsModeDoH = "doh" + dnsModeAuto = "auto" + defaultStreamsPerCache = 10 +) + +// Client holds parsed and validated client CLI options. +type Client struct { + Host string + Port string + Listen string + VKLink string // sanitized: stripped past "join/" and trimmed at /?# + Peer string + N int + UDP bool + Direct bool + VLESSMode bool + VLESSBond bool + WrapMode bool + WrapKey []byte // nil unless WrapMode + GenWrapKey bool + StreamsPerCred int + Debug bool + ManualCaptcha bool + DNSMode string + DNSServers []string // nil when -dns-servers empty +} + +// Server holds parsed and validated server CLI options. +type Server struct { + Listen string + Connect string + VLESSMode bool + VLESSBond bool + WrapMode bool + WrapKey []byte + GenWrapKey bool + Debug bool +} + +// ParseClient parses args (excluding program name) into a Client. +// On flag.ErrHelp it returns (nil, flag.ErrHelp) so the caller can exit cleanly. +func ParseClient(args []string, errOut io.Writer) (*Client, error) { + fs := flag.NewFlagSet("client", flag.ContinueOnError) + if errOut != nil { + fs.SetOutput(errOut) + } + + host := fs.String("turn", "", "override TURN server ip") + port := fs.String("port", "", "override TURN port") + listen := fs.String("listen", "127.0.0.1:9000", "listen on ip:port") + vklink := fs.String("vk-link", "", "VK calls invite link \"https://vk.com/call/join/...\"") + peerAddr := fs.String("peer", "", "peer server address (host:port)") + n := fs.Int("n", 10, "connections to TURN") + udp := fs.Bool("udp", false, "connect to TURN with UDP") + direct := fs.Bool("no-dtls", false, "connect without obfuscation. DO NOT USE") + vlessMode := fs.Bool("vless", false, "VLESS mode: forward TCP connections (for VLESS) instead of UDP packets") + vlessBond := fs.Bool("vless-bond", false, "bond one VLESS TCP connection across all active smux sessions") + wrapMode := fs.Bool("wrap", false, "WRAP mode: ChaCha20-XOR obfuscate DTLS packets before they reach TURN ChannelData") + wrapKeyHex := fs.String("wrap-key", "", "32-byte hex-encoded shared key for -wrap (64 hex chars)") + genWrapKey := fs.Bool("gen-wrap-key", false, "print a fresh 64-character hex key for -wrap-key and exit") + streamsPerCredFlag := fs.Int("streams-per-cred", defaultStreamsPerCache, "number of TURN streams sharing one VK credential cache") + debugFlag := fs.Bool("debug", false, "enable debug logging") + manualCaptchaFlag := fs.Bool("manual-captcha", false, "skip auto captcha solving, use manual mode immediately") + dnsFlag := fs.String("dns", dnsModeAuto, "DNS resolution mode: udp | doh | auto (auto tries UDP/53 first, sticky-fallback to DoH on total failure)") + dnsServersFlag := fs.String("dns-servers", "", "comma-separated UDP/53 DNS servers to use instead of built-in defaults (e.g. carrier resolvers from Android LinkProperties). Format: ip[:port][,ip[:port]...].") + + if err := fs.Parse(args); err != nil { + return nil, err + } + + c := &Client{ + Host: *host, + Port: *port, + Listen: *listen, + Peer: *peerAddr, + N: *n, + UDP: *udp, + Direct: *direct, + VLESSMode: *vlessMode, + VLESSBond: *vlessBond, + WrapMode: *wrapMode, + GenWrapKey: *genWrapKey, + StreamsPerCred: *streamsPerCredFlag, + Debug: *debugFlag, + ManualCaptcha: *manualCaptchaFlag, + DNSMode: *dnsFlag, + } + + switch c.DNSMode { + case dnsModeUDP, dnsModeDoH, dnsModeAuto: + default: + return nil, fmt.Errorf("invalid -dns value %q: must be udp | doh | auto", c.DNSMode) + } + if *dnsServersFlag != "" { + c.DNSServers = strings.Split(*dnsServersFlag, ",") + } + + // gen-wrap-key short-circuits: caller emits the key and exits, so skip + // the rest of validation (no peer / vk-link needed for key gen). + if c.GenWrapKey { + return c, nil + } + + if c.Peer == "" { + return nil, errors.New("need peer address") + } + if *vklink == "" { + return nil, errors.New("need vk-link") + } + if c.WrapMode && c.Direct { + return nil, fmt.Errorf("-wrap requires DTLS; remove -no-dtls") + } + key, err := wrap.DecodeKey(c.WrapMode, *wrapKeyHex) + if err != nil { + return nil, err + } + c.WrapKey = key + if c.StreamsPerCred <= 0 { + return nil, fmt.Errorf("-streams-per-cred must be positive") + } + if c.N <= 0 { + c.N = 10 + } + + parts := strings.Split(*vklink, "join/") + link := parts[len(parts)-1] + if idx := strings.IndexAny(link, "/?#"); idx != -1 { + link = link[:idx] + } + c.VKLink = link + + return c, nil +} + +// ParseServer parses args (excluding program name) into a Server. +func ParseServer(args []string, errOut io.Writer) (*Server, error) { + fs := flag.NewFlagSet("server", flag.ContinueOnError) + if errOut != nil { + fs.SetOutput(errOut) + } + + listen := fs.String("listen", "0.0.0.0:56000", "listen on ip:port") + connect := fs.String("connect", "", "connect to ip:port") + vlessMode := fs.Bool("vless", false, "VLESS mode: forward TCP connections (for VLESS) instead of UDP packets") + vlessBond := fs.Bool("vless-bond", false, "bond one VLESS TCP connection across all active smux sessions") + wrapMode := fs.Bool("wrap", false, "WRAP mode: ChaCha20-XOR obfuscate DTLS packets before they reach TURN ChannelData") + wrapKeyHex := fs.String("wrap-key", "", "32-byte hex-encoded shared key for -wrap (64 hex chars)") + genWrapKey := fs.Bool("gen-wrap-key", false, "print a fresh 64-character hex key for -wrap-key and exit") + debugFlag := fs.Bool("debug", false, "enable debug logging") + + if err := fs.Parse(args); err != nil { + return nil, err + } + + s := &Server{ + Listen: *listen, + Connect: *connect, + VLESSMode: *vlessMode, + VLESSBond: *vlessBond, + WrapMode: *wrapMode, + GenWrapKey: *genWrapKey, + Debug: *debugFlag, + } + + if s.GenWrapKey { + return s, nil + } + + if s.Connect == "" { + return nil, fmt.Errorf("server address is required") + } + key, err := wrap.DecodeKey(s.WrapMode, *wrapKeyHex) + if err != nil { + return nil, err + } + s.WrapKey = key + + return s, nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..b290e1c --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,207 @@ +package config + +import ( + "bytes" + "errors" + "flag" + "io" + "strings" + "testing" +) + +func validClientArgs() []string { + return []string{ + "-peer", "1.2.3.4:5000", + "-vk-link", "https://vk.com/call/join/abcdef", + } +} + +func TestParseClient_Defaults(t *testing.T) { + c, err := ParseClient(validClientArgs(), io.Discard) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if c.Listen != "127.0.0.1:9000" { + t.Errorf("Listen default: %q", c.Listen) + } + if c.N != 10 { + t.Errorf("N default: %d", c.N) + } + if c.DNSMode != "auto" { + t.Errorf("DNSMode default: %q", c.DNSMode) + } + if c.StreamsPerCred != defaultStreamsPerCache { + t.Errorf("StreamsPerCred default: %d", c.StreamsPerCred) + } + if c.VKLink != "abcdef" { + t.Errorf("VKLink: %q (expected abcdef)", c.VKLink) + } + if c.WrapKey != nil { + t.Errorf("WrapKey should be nil when -wrap absent") + } +} + +func TestParseClient_VKLinkStrip(t *testing.T) { + args := []string{ + "-peer", "1.2.3.4:5000", + "-vk-link", "https://vk.com/call/join/CODE123?foo=bar", + } + c, err := ParseClient(args, io.Discard) + if err != nil { + t.Fatal(err) + } + if c.VKLink != "CODE123" { + t.Errorf("VKLink: %q (expected CODE123)", c.VKLink) + } +} + +func TestParseClient_MissingPeer(t *testing.T) { + _, err := ParseClient([]string{"-vk-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) + } +} + +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") { + t.Errorf("expected vk-link error, got %v", err) + } +} + +func TestParseClient_InvalidDNS(t *testing.T) { + args := append(validClientArgs(), "-dns", "garbage") + _, err := ParseClient(args, io.Discard) + if err == nil || !strings.Contains(err.Error(), "invalid -dns") { + t.Errorf("expected dns error, got %v", err) + } +} + +func TestParseClient_WrapDirectConflict(t *testing.T) { + args := append(validClientArgs(), "-wrap", "-no-dtls", "-wrap-key", strings.Repeat("aa", 32)) + _, err := ParseClient(args, io.Discard) + if err == nil || !strings.Contains(err.Error(), "-wrap requires DTLS") { + t.Errorf("expected wrap/direct conflict, got %v", err) + } +} + +func TestParseClient_WrapMissingKey(t *testing.T) { + args := append(validClientArgs(), "-wrap") + _, 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) + } +} + +func TestParseClient_WrapKeyOK(t *testing.T) { + args := append(validClientArgs(), "-wrap", "-wrap-key", strings.Repeat("ab", 32)) + c, err := ParseClient(args, io.Discard) + if err != nil { + t.Fatal(err) + } + if len(c.WrapKey) != 32 { + t.Errorf("WrapKey len: %d", len(c.WrapKey)) + } +} + +func TestParseClient_StreamsPerCredNonPositive(t *testing.T) { + args := append(validClientArgs(), "-streams-per-cred", "0") + _, err := ParseClient(args, io.Discard) + if err == nil || !strings.Contains(err.Error(), "streams-per-cred") { + t.Errorf("expected streams-per-cred error, got %v", err) + } +} + +func TestParseClient_NClampedToTen(t *testing.T) { + args := append(validClientArgs(), "-n", "-5") + c, err := ParseClient(args, io.Discard) + if err != nil { + t.Fatal(err) + } + if c.N != 10 { + t.Errorf("N: %d (expected 10)", c.N) + } +} + +func TestParseClient_DNSServersSplit(t *testing.T) { + args := append(validClientArgs(), "-dns-servers", "1.1.1.1,8.8.8.8:53") + c, err := ParseClient(args, io.Discard) + if err != nil { + t.Fatal(err) + } + if len(c.DNSServers) != 2 || c.DNSServers[0] != "1.1.1.1" || c.DNSServers[1] != "8.8.8.8:53" { + t.Errorf("DNSServers: %v", c.DNSServers) + } +} + +func TestParseClient_GenWrapKeySkipsPeerCheck(t *testing.T) { + c, err := ParseClient([]string{"-gen-wrap-key"}, io.Discard) + if err != nil { + t.Fatalf("gen-wrap-key should not require peer/vk-link: %v", err) + } + if !c.GenWrapKey { + t.Errorf("GenWrapKey not set") + } +} + +func TestParseClient_HelpReturnsErrHelp(t *testing.T) { + var buf bytes.Buffer + _, err := ParseClient([]string{"-h"}, &buf) + if !errors.Is(err, flag.ErrHelp) { + t.Errorf("expected flag.ErrHelp, got %v", err) + } +} + +func TestParseServer_Defaults(t *testing.T) { + s, err := ParseServer([]string{"-connect", "backend:1234"}, io.Discard) + if err != nil { + t.Fatal(err) + } + if s.Listen != "0.0.0.0:56000" { + t.Errorf("Listen default: %q", s.Listen) + } + if s.Connect != "backend:1234" { + t.Errorf("Connect: %q", s.Connect) + } +} + +func TestParseServer_MissingConnect(t *testing.T) { + _, err := ParseServer(nil, io.Discard) + if err == nil || !strings.Contains(err.Error(), "server address") { + t.Errorf("expected connect error, got %v", err) + } +} + +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_WrapKeyBadHex(t *testing.T) { + _, err := ParseServer([]string{"-connect", "x:1", "-wrap", "-wrap-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) + if err != nil { + t.Fatal(err) + } + if len(s.WrapKey) != 32 { + t.Errorf("WrapKey len: %d", len(s.WrapKey)) + } +} + +func TestParseServer_GenWrapKeySkipsConnectCheck(t *testing.T) { + s, err := ParseServer([]string{"-gen-wrap-key"}, io.Discard) + if err != nil { + t.Fatalf("gen-wrap-key should not require -connect: %v", err) + } + if !s.GenWrapKey { + t.Errorf("GenWrapKey not set") + } +} diff --git a/server/main.go b/server/main.go index 920f8e3..15d5eb8 100644 --- a/server/main.go +++ b/server/main.go @@ -2,9 +2,6 @@ package main import ( "context" - "crypto/rand" - "encoding/hex" - "flag" "fmt" "io" "log" @@ -17,6 +14,7 @@ import ( "github.com/cacggghp/vk-turn-proxy/internal/bond" bondserver "github.com/cacggghp/vk-turn-proxy/internal/bond/server" + "github.com/cacggghp/vk-turn-proxy/internal/config" "github.com/cacggghp/vk-turn-proxy/internal/stats" "github.com/cacggghp/vk-turn-proxy/internal/wrap" "github.com/cacggghp/vk-turn-proxy/tcputil" @@ -34,23 +32,18 @@ func debugf(format string, v ...any) { } 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") - vlessBond := flag.Bool("vless-bond", false, "bond one VLESS TCP connection across all active smux sessions") - wrapMode := flag.Bool("wrap", false, "WRAP mode: ChaCha20-XOR obfuscate DTLS packets before they reach TURN ChannelData") - wrapKeyHex := flag.String("wrap-key", "", "32-byte hex-encoded shared key for -wrap (64 hex chars)") - genWrapKey := flag.Bool("gen-wrap-key", false, "print a fresh 64-character hex key for -wrap-key and exit") - debugFlag := flag.Bool("debug", false, "enable debug logging") - flag.Parse() - isDebug = *debugFlag + cfg, err := config.ParseServer(os.Args[1:], os.Stderr) + if err != nil { + log.Panicf("%v", err) + } + isDebug = cfg.Debug - if *genWrapKey { - key := make([]byte, wrap.KeyLen) - if _, err := rand.Read(key); err != nil { - log.Panicf("gen-wrap-key: rand.Read: %v", err) + if cfg.GenWrapKey { + key, gerr := wrap.GenKeyHex() + if gerr != nil { + log.Panicf("gen-wrap-key: %v", gerr) } - fmt.Println(hex.EncodeToString(key)) + fmt.Println(key) return } @@ -66,27 +59,12 @@ func main() { log.Fatalf("Exit...\n") }() - addr, err := net.ResolveUDPAddr("udp", *listen) + addr, err := net.ResolveUDPAddr("udp", cfg.Listen) if err != nil { panic(err) } - if len(*connect) == 0 { - log.Panicf("server address is required") - } - var wrapKey []byte - if *wrapMode { - if *wrapKeyHex == "" { - log.Panicf("-wrap requires -wrap-key") - } - wrapKey, err = hex.DecodeString(*wrapKeyHex) - if err != nil { - log.Panicf("-wrap-key invalid hex: %v", err) - } - if len(wrapKey) != wrap.KeyLen { - log.Panicf("-wrap-key must decode to %d bytes (got %d)", wrap.KeyLen, len(wrapKey)) - } - } - log.Printf("Starting server listen=%s connect=%s vless=%t vless-bond=%t wrap=%t bond-autodetect=true", *listen, *connect, *vlessMode, *vlessBond, *wrapMode) + wrapKey := cfg.WrapKey + log.Printf("Starting server listen=%s connect=%s vless=%t vless-bond=%t wrap=%t bond-autodetect=true", cfg.Listen, cfg.Connect, cfg.VLESSMode, cfg.VLESSBond, cfg.WrapMode) // Generate a certificate and private key to secure the connection certificate, genErr := selfsign.GenerateSelfSigned() if genErr != nil { @@ -104,7 +82,7 @@ func main() { dtls.WithConnectionIDGenerator(dtls.RandomCIDGenerator(8)), } var listener net.Listener - if *wrapMode { + if cfg.WrapMode { log.Printf("WRAP mode enabled: listener only accepts clients with matching -wrap-key") wrapListener, werr := wrap.Listen(addr, wrapKey) if werr != nil { @@ -163,10 +141,10 @@ func main() { } debugf("Handshake done") - if *vlessMode { - handleVLESSConnection(ctx, dtlsConn, *connect, *vlessBond) + if cfg.VLESSMode { + handleVLESSConnection(ctx, dtlsConn, cfg.Connect, cfg.VLESSBond) } else { - handleUDPConnection(ctx, conn, *connect) + handleUDPConnection(ctx, conn, cfg.Connect) } debugf("Connection closed: %s\n", conn.RemoteAddr())