mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
refactor: вынести разбор CLI в internal/config
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-контракта.
This commit is contained in:
+29
-76
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
+18
-40
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user