|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
// TODO: 实现透明代理
|
|
|
|
|
logger.Error("❌ Transparent proxy mode is not yet implemented")
|
|
|
|
|
logger.Info("💡 Available alternatives:")
|
|
|
|
|
logger.Info(" - Use global mode: ./bin/wormhole-client -mode global")
|
|
|
|
|
logger.Info(" - Use HTTP mode: ./bin/wormhole-client -mode http")
|
|
|
|
|
|
|
|
|
|
return fmt.Errorf("transparent proxy mode not implemented")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|