修复客户端核心问题: HTTP代理路由逻辑、SOCKS5统计功能、CONNECT请求处理优化,浏览器访问和统计监控现已正常工作

main
huyinsong 2 weeks ago
parent 4c0aeeb96d
commit 0376a5f39d
  1. 63
      configs/client.yaml
  2. 417
      internal/client/client.go
  3. 25
      internal/proxy/socks5.go

@ -3,15 +3,15 @@ serviceType: client
# SOCKS5 服务器设置 # SOCKS5 服务器设置
server: server:
address: 127.0.0.1 address: 3.133.130.202
port: 1080 port: 1080
username: admin username: admin
password: secure_password_123 password: secure123
# 代理模式设置 # 代理模式设置
proxy: proxy:
mode: global # http, global, transparent mode: global # http, global, transparent
localPort: 8080 localPort: 9090
# 全局代理设置 # 全局代理设置
globalProxy: globalProxy:
@ -60,62 +60,117 @@ globalProxy:
# 强制代理域名列表 (必须经过代理) # 强制代理域名列表 (必须经过代理)
forceDomains: forceDomains:
# Google 服务 # Google 服务
- "google.com"
- "*.google.com" - "*.google.com"
- "googlepai.com"
- "*.googlepai.com" - "*.googlepai.com"
- "googleapis.com"
- "*.googleapis.com" - "*.googleapis.com"
- "googleusercontent.com"
- "*.googleusercontent.com" - "*.googleusercontent.com"
- "googlevideo.com"
- "*.googlevideo.com" - "*.googlevideo.com"
- "gstatic.com"
- "*.gstatic.com" - "*.gstatic.com"
- "gmail.com"
- "*.gmail.com" - "*.gmail.com"
- "youtube.com"
- "*.youtube.com" - "*.youtube.com"
- "youtu.be"
- "*.youtu.be" - "*.youtu.be"
- "ytimg.com"
- "*.ytimg.com" - "*.ytimg.com"
# 社交媒体 # 社交媒体
- "facebook.com"
- "*.facebook.com" - "*.facebook.com"
- "fbcdn.net"
- "*.fbcdn.net" - "*.fbcdn.net"
- "instagram.com"
- "*.instagram.com" - "*.instagram.com"
- "twitter.com"
- "*.twitter.com" - "*.twitter.com"
- "twimg.com"
- "*.twimg.com" - "*.twimg.com"
- "t.co"
- "*.t.co" - "*.t.co"
- "linkedin.com"
- "*.linkedin.com" - "*.linkedin.com"
- "pinterest.com"
- "*.pinterest.com" - "*.pinterest.com"
- "reddit.com"
- "*.reddit.com" - "*.reddit.com"
- "snapchat.com"
- "*.snapchat.com" - "*.snapchat.com"
- "discord.com"
- "*.discord.com" - "*.discord.com"
- "telegram.org"
- "*.telegram.org" - "*.telegram.org"
- "whatsapp.com"
- "*.whatsapp.com" - "*.whatsapp.com"
# 技术开发 # 技术开发
- "github.com"
- "*.github.com" - "*.github.com"
- "githubusercontent.com"
- "*.githubusercontent.com" - "*.githubusercontent.com"
- "github.io"
- "*.github.io" - "*.github.io"
- "stackoverflow.com"
- "*.stackoverflow.com" - "*.stackoverflow.com"
- "stackexchange.com"
- "*.stackexchange.com" - "*.stackexchange.com"
- "medium.com"
- "*.medium.com" - "*.medium.com"
- "dev.to"
- "*.dev.to" - "*.dev.to"
- "npmjs.com"
- "*.npmjs.com" - "*.npmjs.com"
- "pypi.org"
- "*.pypi.org" - "*.pypi.org"
- "docker.com"
- "*.docker.com" - "*.docker.com"
- "hub.docker.com"
- "*.hub.docker.com" - "*.hub.docker.com"
- "docker.io"
- "*.docker.io"
- "registry-1.docker.io"
- "auth.docker.io"
- "registry.docker.io"
- "index.docker.io"
# 新闻媒体 # 新闻媒体
- "nytimes.com"
- "*.nytimes.com" - "*.nytimes.com"
- "washingtonpost.com"
- "*.washingtonpost.com" - "*.washingtonpost.com"
- "wsj.com"
- "*.wsj.com" - "*.wsj.com"
- "reuters.com"
- "*.reuters.com" - "*.reuters.com"
- "bbc.com"
- "*.bbc.com" - "*.bbc.com"
- "cnn.com"
- "*.cnn.com" - "*.cnn.com"
- "bloomberg.com"
- "*.bloomberg.com" - "*.bloomberg.com"
# 其他服务 # 其他服务
- "dropbox.com"
- "*.dropbox.com" - "*.dropbox.com"
- "onedrive.com"
- "*.onedrive.com" - "*.onedrive.com"
- "zoom.us"
- "*.zoom.us" - "*.zoom.us"
- "spotify.com"
- "*.spotify.com" - "*.spotify.com"
- "netflix.com"
- "*.netflix.com" - "*.netflix.com"
- "hulu.com"
- "*.hulu.com" - "*.hulu.com"
- "twitch.tv"
- "*.twitch.tv" - "*.twitch.tv"
- "steam.community"
- "*.steam.community" - "*.steam.community"
# 透明代理设置 (实验性功能) # 透明代理设置 (实验性功能)
@ -197,7 +252,7 @@ timeout: 30s
# Web管理界面 (可选) # Web管理界面 (可选)
webUI: webUI:
enabled: false enabled: true
port: 8081 port: 8081
username: "admin" username: "admin"
password: "wormhole123" password: "wormhole123"

@ -1,8 +1,12 @@
package client package client
import ( import (
"bufio"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io"
"net"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -10,6 +14,7 @@ import (
"time" "time"
"github.com/azoic/wormhole-client/internal/config" "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/proxy"
"github.com/azoic/wormhole-client/internal/routing" "github.com/azoic/wormhole-client/internal/routing"
"github.com/azoic/wormhole-client/internal/system" "github.com/azoic/wormhole-client/internal/system"
@ -24,6 +29,7 @@ type Client struct {
dnsProxy *dns.DNSProxy dnsProxy *dns.DNSProxy
systemProxyMgr *system.SystemProxyManager systemProxyMgr *system.SystemProxyManager
routeMatcher *routing.RouteMatcher routeMatcher *routing.RouteMatcher
guiServer *gui.GUIServer
httpServer *http.Server httpServer *http.Server
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
@ -78,6 +84,9 @@ func (c *Client) Start(configPath string) error {
} }
} }
// 创建 GUI 服务器
c.guiServer = gui.NewGUIServer(cfg, c.socks5Proxy, c.systemProxyMgr, c.routeMatcher, c.mode)
// 设置信号处理 // 设置信号处理
c.setupSignalHandler() c.setupSignalHandler()
@ -97,12 +106,13 @@ func (c *Client) startHTTPProxy() error {
logger.Info("🌐 Starting HTTP proxy mode...") logger.Info("🌐 Starting HTTP proxy mode...")
logger.Info("💡 Setting up HTTP proxy on port %d", c.config.Proxy.LocalPort) logger.Info("💡 Setting up HTTP proxy on port %d", c.config.Proxy.LocalPort)
c.httpServer = c.socks5Proxy.CreateHTTPProxy(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("✅ 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("💡 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("📊 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("💚 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() return c.httpServer.ListenAndServe()
} }
@ -113,7 +123,7 @@ func (c *Client) startGlobalProxy() error {
logger.Info("⚠ Requires administrator privileges") logger.Info("⚠ Requires administrator privileges")
// 启动HTTP代理服务器 // 启动HTTP代理服务器
c.httpServer = c.socks5Proxy.CreateHTTPProxy(c.config.Proxy.LocalPort) c.httpServer = c.createHTTPServerWithGUI(c.config.Proxy.LocalPort)
errChan := make(chan error, 1) errChan := make(chan error, 1)
go func() { go func() {
@ -171,6 +181,7 @@ func (c *Client) startGlobalProxy() error {
} }
logger.Info("📊 Statistics: http://%s/stats", httpProxy) logger.Info("📊 Statistics: http://%s/stats", httpProxy)
logger.Info("💚 Health check: http://%s/health", httpProxy) logger.Info("💚 Health check: http://%s/health", httpProxy)
logger.Info("🖥 Web GUI: http://%s/gui", httpProxy)
logger.Info("🛑 Press Ctrl+C to stop") logger.Info("🛑 Press Ctrl+C to stop")
// 检查是否有错误 // 检查是否有错误
@ -196,6 +207,408 @@ func (c *Client) startTransparentProxy() error {
return fmt.Errorf("transparent proxy mode not implemented") 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) {
// 确保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"
}
}
// 使用路由匹配器判断是否需要代理
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()
// 发送HTTP请求
if err := r.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), r)
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() { func (c *Client) setupSignalHandler() {
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

@ -278,6 +278,31 @@ func (p *SOCKS5Proxy) GetStats() ProxyStatsSnapshot {
return p.stats.GetStats() return p.stats.GetStats()
} }
// IncrementConnections 增加连接计数
func (p *SOCKS5Proxy) IncrementConnections() {
p.stats.IncrementConnections()
}
// DecrementActiveConnections 减少活跃连接计数
func (p *SOCKS5Proxy) DecrementActiveConnections() {
p.stats.DecrementActiveConnections()
}
// IncrementSuccessfulRequests 增加成功请求计数
func (p *SOCKS5Proxy) IncrementSuccessfulRequests() {
p.stats.IncrementSuccessfulRequests()
}
// IncrementFailedRequests 增加失败请求计数
func (p *SOCKS5Proxy) IncrementFailedRequests() {
p.stats.IncrementFailedRequests()
}
// AddBytesTransferred 添加传输字节数
func (p *SOCKS5Proxy) AddBytesTransferred(sent, received int64) {
p.stats.AddBytesTransferred(sent, received)
}
// DialTCP 通过SOCKS5连接到目标地址 // DialTCP 通过SOCKS5连接到目标地址
func (p *SOCKS5Proxy) DialTCP(address string) (net.Conn, error) { func (p *SOCKS5Proxy) DialTCP(address string) (net.Conn, error) {
return p.DialTCPWithContext(context.Background(), address) return p.DialTCPWithContext(context.Background(), address)

Loading…
Cancel
Save