package client import ( "bufio" "context" "encoding/json" "fmt" "io" "net" "net/http" "os" "os/signal" "syscall" "time" "github.com/azoic/wormhole-client/internal/config" "github.com/azoic/wormhole-client/internal/gui" "github.com/azoic/wormhole-client/internal/proxy" "github.com/azoic/wormhole-client/internal/routing" "github.com/azoic/wormhole-client/internal/system" "github.com/azoic/wormhole-client/pkg/dns" "github.com/azoic/wormhole-client/pkg/logger" ) type Client struct { mode string config *config.Config socks5Proxy *proxy.SOCKS5Proxy dnsProxy *dns.DNSProxy systemProxyMgr *system.SystemProxyManager routeMatcher *routing.RouteMatcher guiServer *gui.GUIServer httpServer *http.Server ctx context.Context cancel context.CancelFunc } func NewClient(mode string) *Client { ctx, cancel := context.WithCancel(context.Background()) return &Client{ mode: mode, systemProxyMgr: system.NewSystemProxyManager(), ctx: ctx, cancel: cancel, } } func (c *Client) Start(configPath string) error { // 加载配置 cfg, err := config.LoadConfig(configPath) if err != nil { return fmt.Errorf("failed to load config: %v", err) } if err := cfg.Validate(); err != nil { return fmt.Errorf("invalid config: %v", err) } c.config = cfg // 设置日志级别 logger.SetLevel(cfg.LogLevel) logger.Info("🚀 Starting Wormhole SOCKS5 Client") logger.Info("📄 Config loaded from: %s", configPath) logger.Info("🔧 Mode: %s", c.mode) logger.Info("🎯 SOCKS5 Server: %s", cfg.GetServerAddr()) // 创建SOCKS5代理客户端 c.socks5Proxy = proxy.NewSOCKS5Proxy( cfg.GetServerAddr(), cfg.Server.Username, cfg.Server.Password, cfg.Timeout, ) // 初始化路由匹配器(如果有路由配置) if c.mode == "global" && len(cfg.GlobalProxy.Routing.BypassDomains) > 0 || len(cfg.GlobalProxy.Routing.ForceDomains) > 0 { c.routeMatcher, err = routing.NewRouteMatcher(&cfg.GlobalProxy.Routing) if err != nil { logger.Warn("Failed to initialize route matcher: %v", err) } else { logger.Info("🛣️ Route matcher initialized") } } // 创建 GUI 服务器 c.guiServer = gui.NewGUIServer(cfg, c.socks5Proxy, c.systemProxyMgr, c.routeMatcher, c.mode) // 设置信号处理 c.setupSignalHandler() switch c.mode { case "http": return c.startHTTPProxy() case "global": return c.startGlobalProxy() case "transparent": return c.startTransparentProxy() default: return fmt.Errorf("unsupported mode: %s", c.mode) } } func (c *Client) startHTTPProxy() error { logger.Info("🌐 Starting HTTP proxy mode...") logger.Info("💡 Setting up HTTP proxy on port %d", c.config.Proxy.LocalPort) c.httpServer = c.createHTTPServerWithGUI(c.config.Proxy.LocalPort) logger.Info("✅ HTTP proxy started on :%d", c.config.Proxy.LocalPort) logger.Info("💡 Configure your browser to use HTTP proxy: 127.0.0.1:%d", c.config.Proxy.LocalPort) logger.Info("📊 Statistics available at: http://127.0.0.1:%d/stats", c.config.Proxy.LocalPort) logger.Info("💚 Health check available at: http://127.0.0.1:%d/health", c.config.Proxy.LocalPort) logger.Info("🖥️ Web GUI available at: http://127.0.0.1:%d/gui", c.config.Proxy.LocalPort) return c.httpServer.ListenAndServe() } func (c *Client) startGlobalProxy() error { logger.Info("🌍 Starting global proxy mode...") logger.Info("💡 This will configure system-wide proxy settings") logger.Info("⚠️ Requires administrator privileges") // 启动HTTP代理服务器 c.httpServer = c.createHTTPServerWithGUI(c.config.Proxy.LocalPort) errChan := make(chan error, 1) go func() { if err := c.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { errChan <- fmt.Errorf("HTTP proxy server failed: %v", err) } }() // 等待服务器启动 time.Sleep(500 * time.Millisecond) // 设置系统代理 httpProxy := fmt.Sprintf("127.0.0.1:%d", c.config.Proxy.LocalPort) httpsProxy := httpProxy logger.Info("📡 Setting system proxy to %s", httpProxy) if err := c.systemProxyMgr.SetGlobalProxy(httpProxy, httpsProxy, ""); err != nil { logger.Error("Failed to set system proxy: %v", err) logger.Warn("💡 You may need to run with administrator privileges") logger.Info("📋 Manual setup instructions:") logger.Info(" - Set HTTP proxy to: %s", httpProxy) logger.Info(" - Set HTTPS proxy to: %s", httpsProxy) } else { logger.Info("✅ System proxy configured successfully") } // 启动DNS代理(如果启用) if c.config.GlobalProxy.DNSProxy { logger.Info("🔍 Starting DNS proxy on port %d", c.config.GlobalProxy.DNSPort) // 创建DNS代理(通过SOCKS5转发DNS查询) c.dnsProxy = dns.NewDNSProxy("8.8.8.8:53", c.config.GlobalProxy.DNSPort) if err := c.dnsProxy.Start(); err != nil { logger.Warn("Failed to start DNS proxy: %v", err) } else { logger.Info("✅ DNS proxy started") logger.Info("💡 Configure your system DNS to: 127.0.0.1:%d", c.config.GlobalProxy.DNSPort) } } // 显示路由配置信息 if c.routeMatcher != nil { stats := c.routeMatcher.GetStats() logger.Info("🛣️ Routing configuration:") logger.Info(" - Bypass domains: %v", stats["bypass_domains_count"]) logger.Info(" - Force domains: %v", stats["force_domains_count"]) logger.Info(" - Bypass local: %v", stats["bypass_local"]) logger.Info(" - Bypass private: %v", stats["bypass_private"]) } logger.Info("🎉 Global proxy mode started successfully") logger.Info("📍 HTTP/HTTPS Proxy: %s", httpProxy) if c.dnsProxy != nil { logger.Info("📍 DNS Proxy: 127.0.0.1:%d", c.config.GlobalProxy.DNSPort) } logger.Info("📊 Statistics: http://%s/stats", httpProxy) logger.Info("💚 Health check: http://%s/health", httpProxy) logger.Info("🖥️ Web GUI: http://%s/gui", httpProxy) logger.Info("🛑 Press Ctrl+C to stop") // 检查是否有错误 select { case err := <-errChan: return err case <-c.ctx.Done(): return nil } } func (c *Client) startTransparentProxy() error { logger.Info("🔍 Starting transparent proxy mode...") logger.Info("💡 This will intercept network traffic transparently") logger.Info("⚠️ Requires root privileges and iptables support") // 暂时禁用透明代理模式,提供更好的用户体验 logger.Warn("⚠️ Transparent proxy mode is currently disabled") logger.Info("🔄 This feature is under development and not yet ready for production use") logger.Info("💡 Available alternatives:") logger.Info(" - Use global mode: ./bin/wormhole-client -mode global") logger.Info(" - Use HTTP mode: ./bin/wormhole-client -mode http") logger.Info("📝 Note: Global mode provides similar functionality with system proxy configuration") return fmt.Errorf("transparent proxy mode is currently disabled - please use 'global' or 'http' mode") } // createHTTPServerWithGUI 创建带有 GUI 的 HTTP 服务器 func (c *Client) createHTTPServerWithGUI(localPort int) *http.Server { // 创建ServeMux来处理不同的路径 mux := http.NewServeMux() // 代理处理器 proxyHandler := &httpProxyHandler{ socks5Proxy: c.socks5Proxy, routeMatcher: c.routeMatcher, } // API 路由 mux.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } stats := c.socks5Proxy.GetStats() w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") if err := json.NewEncoder(w).Encode(stats); err != nil { logger.Error("Failed to encode stats: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } }) mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } stats := c.socks5Proxy.GetStats() health := map[string]interface{}{ "status": "healthy", "uptime": stats.Uptime.String(), "active_connections": stats.ActiveConnections, "success_rate": stats.GetSuccessRate(), } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(health); err != nil { logger.Error("Failed to encode health: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } }) // GUI 路由 if c.guiServer != nil { c.guiServer.RegisterRoutes(mux) } // 创建一个包装器来处理CONNECT请求 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 对于CONNECT请求,直接使用代理处理器 if r.Method == http.MethodConnect { proxyHandler.ServeHTTP(w, r) return } // 对于其他请求,使用ServeMux mux.ServeHTTP(w, r) }) server := &http.Server{ Addr: fmt.Sprintf(":%d", localPort), Handler: handler, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, MaxHeaderBytes: 1 << 20, // 1MB } return server } // httpProxyHandler HTTP代理处理器 type httpProxyHandler struct { socks5Proxy *proxy.SOCKS5Proxy routeMatcher *routing.RouteMatcher } func (h *httpProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 添加调试日志 logger.Info("Received request: Method=%s, URL.Path=%s, Host=%s", r.Method, r.URL.Path, r.Host) // 只对非CONNECT请求检查API和GUI路径 if r.Method != http.MethodConnect && isAPIOrGUIPath(r.URL.Path) { logger.Info("Request blocked by isAPIOrGUIPath: %s", r.URL.Path) http.NotFound(w, r) return } // 统计连接 - 增加连接计数 h.socks5Proxy.IncrementConnections() // 使用defer确保在请求结束时减少活跃连接数 defer h.socks5Proxy.DecrementActiveConnections() // 调用代理处理逻辑 h.handleProxyRequest(w, r) } // handleProxyRequest 处理代理请求 func (h *httpProxyHandler) handleProxyRequest(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodConnect { h.handleHTTPSProxy(w, r) } else { h.handleHTTPProxy(w, r) } } // handleHTTPSProxy 处理HTTPS代理请求 (CONNECT方法) func (h *httpProxyHandler) handleHTTPSProxy(w http.ResponseWriter, r *http.Request) { // 解析目标主机 host, port, err := net.SplitHostPort(r.Host) if err != nil { // 如果没有端口,默认使用443 host = r.Host port = "443" } // 使用路由匹配器判断是否需要代理 if h.routeMatcher != nil { matchResult := h.routeMatcher.Match(host) switch matchResult { case routing.MatchBypass: // 直连 logger.Debug("HTTPS %s: Direct connection (bypass)", host) h.handleDirectHTTPSProxy(w, r, host, port) return case routing.MatchProxy: // 强制代理 logger.Debug("HTTPS %s: Using SOCKS5 proxy (force)", host) case routing.MatchAuto: // 自动决定,默认使用代理 logger.Debug("HTTPS %s: Using SOCKS5 proxy (auto)", host) } } // 通过SOCKS5代理连接 targetHost := net.JoinHostPort(host, port) destConn, err := h.socks5Proxy.DialTCP(targetHost) if err != nil { logger.Error("Failed to connect via SOCKS5 to %s: %v", targetHost, err) h.socks5Proxy.IncrementFailedRequests() http.Error(w, "Bad Gateway", http.StatusBadGateway) return } defer destConn.Close() // 发送200 Connection established响应 w.WriteHeader(http.StatusOK) hijacker, ok := w.(http.Hijacker) if !ok { logger.Error("Hijacking not supported") h.socks5Proxy.IncrementFailedRequests() http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } clientConn, _, err := hijacker.Hijack() if err != nil { logger.Error("Failed to hijack connection: %v", err) h.socks5Proxy.IncrementFailedRequests() return } defer clientConn.Close() // 记录成功建立连接 h.socks5Proxy.IncrementSuccessfulRequests() // 双向数据转发 go func() { defer destConn.Close() defer clientConn.Close() written, err := io.Copy(destConn, clientConn) if err == nil && written > 0 { h.socks5Proxy.AddBytesTransferred(written, 0) } }() written, err := io.Copy(clientConn, destConn) if err == nil && written > 0 { h.socks5Proxy.AddBytesTransferred(0, written) } } // handleDirectHTTPSProxy 直接连接处理HTTPS代理 func (h *httpProxyHandler) handleDirectHTTPSProxy(w http.ResponseWriter, r *http.Request, host, port string) { // 直接连接到目标服务器 targetAddr := net.JoinHostPort(host, port) destConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second) if err != nil { logger.Error("Failed to connect directly to %s: %v", targetAddr, err) h.socks5Proxy.IncrementFailedRequests() http.Error(w, "Bad Gateway", http.StatusBadGateway) return } defer destConn.Close() // 发送200 Connection established响应 w.WriteHeader(http.StatusOK) hijacker, ok := w.(http.Hijacker) if !ok { logger.Error("Hijacking not supported") h.socks5Proxy.IncrementFailedRequests() http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } clientConn, _, err := hijacker.Hijack() if err != nil { logger.Error("Failed to hijack connection: %v", err) h.socks5Proxy.IncrementFailedRequests() return } defer clientConn.Close() // 记录成功建立连接 h.socks5Proxy.IncrementSuccessfulRequests() // 双向数据转发 go func() { defer destConn.Close() defer clientConn.Close() written, err := io.Copy(destConn, clientConn) if err == nil && written > 0 { h.socks5Proxy.AddBytesTransferred(written, 0) } }() written, err := io.Copy(clientConn, destConn) if err == nil && written > 0 { h.socks5Proxy.AddBytesTransferred(0, written) } } // handleHTTPProxy 处理HTTP代理请求 func (h *httpProxyHandler) handleHTTPProxy(w http.ResponseWriter, r *http.Request) { logger.Info("Processing HTTP proxy request: %s %s", r.Method, r.URL.String()) // 确保URL包含Host if r.URL.Host == "" { r.URL.Host = r.Host } if r.URL.Scheme == "" { r.URL.Scheme = "http" } // 解析目标主机 host, port, err := net.SplitHostPort(r.Host) if err != nil { // 如果没有端口,根据协议添加默认端口 host = r.Host if r.URL.Scheme == "https" { port = "443" } else { port = "80" } } logger.Info("Target host: %s, port: %s", host, port) // 使用路由匹配器判断是否需要代理 if h.routeMatcher != nil { matchResult := h.routeMatcher.Match(host) switch matchResult { case routing.MatchBypass: // 直连 logger.Debug("HTTP %s: Direct connection (bypass)", host) h.handleDirectHTTPProxy(w, r) return case routing.MatchProxy: // 强制代理 logger.Debug("HTTP %s: Using SOCKS5 proxy (force)", host) case routing.MatchAuto: // 自动决定,默认使用代理 logger.Debug("HTTP %s: Using SOCKS5 proxy (auto)", host) } } // 通过SOCKS5代理连接 targetHost := net.JoinHostPort(host, port) destConn, err := h.socks5Proxy.DialTCP(targetHost) if err != nil { logger.Error("Failed to connect via SOCKS5 to %s: %v", targetHost, err) h.socks5Proxy.IncrementFailedRequests() http.Error(w, "Bad Gateway", http.StatusBadGateway) return } defer destConn.Close() // 创建新的请求用于转发到目标服务器 // 需要将代理格式的URL转换为相对路径格式 requestURL := r.URL.Path if r.URL.RawQuery != "" { requestURL += "?" + r.URL.RawQuery } if requestURL == "" { requestURL = "/" } logger.Info("Original URL: %s, Converted URL: %s", r.URL.String(), requestURL) newReq, err := http.NewRequest(r.Method, requestURL, r.Body) if err != nil { logger.Error("Failed to create request for SOCKS5 proxy: %v", err) h.socks5Proxy.IncrementFailedRequests() http.Error(w, "Bad Request", http.StatusBadRequest) return } // 复制请求头,但需要调整Host头 for key, values := range r.Header { if key == "Host" { continue // 跳过Host头,使用目标服务器的Host } for _, value := range values { newReq.Header.Add(key, value) } } // 设置正确的Host头 newReq.Host = r.Host newReq.Header.Set("Host", r.Host) // 发送HTTP请求 if err := newReq.Write(destConn); err != nil { logger.Error("Failed to write request to SOCKS5 connection: %v", err) h.socks5Proxy.IncrementFailedRequests() http.Error(w, "Bad Gateway", http.StatusBadGateway) return } // 读取并转发响应 resp, err := http.ReadResponse(bufio.NewReader(destConn), newReq) if err != nil { logger.Error("Failed to read response from SOCKS5 connection: %v", err) h.socks5Proxy.IncrementFailedRequests() http.Error(w, "Bad Gateway", http.StatusBadGateway) return } defer resp.Body.Close() // 记录成功请求 h.socks5Proxy.IncrementSuccessfulRequests() // 复制响应头 for key, values := range resp.Header { for _, value := range values { w.Header().Add(key, value) } } // 设置状态码 w.WriteHeader(resp.StatusCode) // 复制响应体并统计字节数 written, err := io.Copy(w, resp.Body) if err == nil && written > 0 { h.socks5Proxy.AddBytesTransferred(0, written) } } // handleDirectHTTPProxy 直接连接处理HTTP代理 func (h *httpProxyHandler) handleDirectHTTPProxy(w http.ResponseWriter, r *http.Request) { // 创建直接连接的HTTP客户端 client := &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 10 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, }, } // 创建新的请求 newReq, err := http.NewRequest(r.Method, r.URL.String(), r.Body) if err != nil { logger.Error("Failed to create direct request: %v", err) http.Error(w, "Bad Request", http.StatusBadRequest) return } // 复制请求头 for key, values := range r.Header { for _, value := range values { newReq.Header.Add(key, value) } } // 发送请求 resp, err := client.Do(newReq) if err != nil { logger.Error("Failed to make direct request: %v", err) http.Error(w, "Bad Gateway", http.StatusBadGateway) return } defer resp.Body.Close() // 复制响应头 for key, values := range resp.Header { for _, value := range values { w.Header().Add(key, value) } } // 设置状态码 w.WriteHeader(resp.StatusCode) // 复制响应体 if _, err := io.Copy(w, resp.Body); err != nil { logger.Error("Failed to copy response body: %v", err) return } } // isAPIOrGUIPath 检查是否为API或GUI路径 func isAPIOrGUIPath(path string) bool { apiPaths := []string{"/stats", "/health", "/api/", "/gui", "/static/"} for _, apiPath := range apiPaths { if path == apiPath || (apiPath[len(apiPath)-1] == '/' && len(path) > len(apiPath) && path[:len(apiPath)] == apiPath) { return true } } return false } func (c *Client) setupSignalHandler() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { <-sigChan logger.Info("🛑 Received shutdown signal...") c.shutdown() }() } func (c *Client) shutdown() { logger.Info("🔄 Shutting down gracefully...") // 停止HTTP服务器 if c.httpServer != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := c.httpServer.Shutdown(ctx); err != nil { logger.Error("Failed to shutdown HTTP server gracefully: %v", err) } else { logger.Info("✅ HTTP server stopped") } } // 恢复系统代理设置 if c.systemProxyMgr != nil && c.systemProxyMgr.IsEnabled() { logger.Info("🔄 Restoring system proxy settings...") if err := c.systemProxyMgr.RestoreProxy(); err != nil { logger.Error("Failed to restore system proxy: %v", err) } } // 停止DNS代理 if c.dnsProxy != nil { logger.Info("🔄 Stopping DNS proxy...") if err := c.dnsProxy.Stop(); err != nil { logger.Error("Failed to stop DNS proxy: %v", err) } else { logger.Info("✅ DNS proxy stopped") } } // 显示统计信息 if c.socks5Proxy != nil { stats := c.socks5Proxy.GetStats() logger.Info("📊 Final statistics:") logger.Info(" - Total connections: %d", stats.TotalConnections) logger.Info(" - Successful requests: %d", stats.SuccessfulRequests) logger.Info(" - Failed requests: %d", stats.FailedRequests) logger.Info(" - Success rate: %.2f%%", stats.GetSuccessRate()) logger.Info(" - Total bytes: %d", stats.GetTotalBytes()) logger.Info(" - Uptime: %v", stats.Uptime) } logger.Info("✅ Cleanup completed") // 取消上下文 c.cancel() // 给一点时间让goroutines优雅退出 time.Sleep(100 * time.Millisecond) os.Exit(0) } // GetStats 获取客户端统计信息 func (c *Client) GetStats() map[string]interface{} { stats := make(map[string]interface{}) if c.socks5Proxy != nil { proxyStats := c.socks5Proxy.GetStats() stats["proxy"] = proxyStats } if c.systemProxyMgr != nil { stats["system_proxy_enabled"] = c.systemProxyMgr.IsEnabled() if currentProxy, err := c.systemProxyMgr.GetCurrentProxy(); err == nil { stats["current_system_proxy"] = currentProxy } } if c.routeMatcher != nil { stats["routing"] = c.routeMatcher.GetStats() } stats["mode"] = c.mode stats["config_file"] = c.config return stats }