diff --git a/Makefile b/Makefile
index 8bd5d21..a92a9a3 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ APP_NAME = wormhole-client
VERSION = v1.0.0
LDFLAGS = -ldflags "-X main.version=$(VERSION) -X main.buildTime=$(shell date -u '+%Y-%m-%d_%H:%M:%S')"
-.PHONY: all build clean deps test run install uninstall lint coverage fmt vet check
+.PHONY: all build clean deps test run install uninstall lint coverage fmt vet check dmg dmg-macos
all: clean deps build
@@ -57,6 +57,28 @@ install: build
uninstall:
sudo rm -f /usr/local/bin/$(APP_NAME)
+# macOS DMG 安装包构建
+dmg-macos:
+ @echo "🍎 Building macOS DMG installer..."
+ @if [ "$(shell uname)" != "Darwin" ]; then \
+ echo "❌ DMG building is only supported on macOS"; \
+ exit 1; \
+ fi
+ chmod +x scripts/build-dmg.sh
+ ./scripts/build-dmg.sh
+ @echo "✅ macOS DMG installer created in build/ directory"
+
+# 通用 DMG 构建 (检测平台)
+dmg:
+ @if [ "$(shell uname)" = "Darwin" ]; then \
+ $(MAKE) dmg-macos; \
+ else \
+ echo "❌ DMG building is only supported on macOS"; \
+ echo "ℹ️ You are on: $(shell uname)"; \
+ echo "ℹ️ Please use a macOS system to build DMG installers"; \
+ exit 1; \
+ fi
+
help:
@echo "Available targets:"
@echo " build - Build the client binary"
@@ -73,6 +95,8 @@ help:
@echo " deps - Download dependencies"
@echo " install - Install to /usr/local/bin"
@echo " uninstall - Remove from /usr/local/bin"
+ @echo " dmg - Build macOS DMG installer (macOS only)"
+ @echo " dmg-macos - Build macOS DMG installer (macOS only)"
@echo ""
@echo "GUI Access:"
@echo " After starting, access the web GUI at:"
diff --git a/assets/icons/app.icns b/assets/icons/app.icns
new file mode 100644
index 0000000..a1e51b3
Binary files /dev/null and b/assets/icons/app.icns differ
diff --git a/configs/client.yaml b/configs/client.yaml
index 4eeda9d..c22c91f 100644
--- a/configs/client.yaml
+++ b/configs/client.yaml
@@ -3,10 +3,12 @@ serviceType: client
# SOCKS5 服务器设置
server:
- address: 3.133.130.202
+ address: 18.117.71.98
port: 1080
username: admin
password: secure123
+ preferIPv4: true # 强制使用IPv4
+ timeout: 60s # 增加连接超时
# 代理模式设置
proxy:
@@ -17,7 +19,7 @@ proxy:
globalProxy:
enabled: true
dnsProxy: true
- dnsPort: 5353
+ dnsPort: 15353 # 客户端DNS代理端口
# 智能分流路由规则
routing:
@@ -56,7 +58,11 @@ globalProxy:
- "*.amazonaws.com"
- "*.aliyuncs.com"
- "*.qcloud.com"
-
+ - "*.cursor.sh"
+ - "*.apple.com"
+ - "*.icloud.com"
+ # 特定服务IP地址
+ - "101.34.16.52" # Gitea 服务器
# 强制代理域名列表 (必须经过代理)
forceDomains:
# Google 服务
@@ -172,15 +178,12 @@ globalProxy:
- "*.twitch.tv"
- "steam.community"
- "*.steam.community"
-
- # 特定服务IP地址
- - "101.34.16.52" # Gitea 服务器
# 透明代理设置 (实验性功能)
transparentProxy:
enabled: false
port: 8080
- dnsPort: 5353
+ dnsPort: 15353 # 透明代理DNS端口(与全局代理一致)
# 系统级修改 (需要root权限)
modifyDNS: true # 修改系统DNS设置
diff --git a/coverage.html b/coverage.html
deleted file mode 100644
index c203ced..0000000
--- a/coverage.html
+++ /dev/null
@@ -1,1693 +0,0 @@
-
-
-
-
-
-
-
package main
-
-import (
- "flag"
- "fmt"
- "log"
- "os"
-
- "github.com/azoic/wormhole-client/internal/client"
-)
-
-var (
- version = "v1.0.0"
- buildTime = "unknown"
-)
-
-func main() {
- configPath := flag.String("config", "configs/client.yaml", "Configuration file path")
- mode := flag.String("mode", "http", "Client mode: http, global, transparent")
- showVersion := flag.Bool("version", false, "Show version information")
- flag.Parse()
-
- if *showVersion {
- fmt.Printf("Wormhole SOCKS5 Client %s\n", version)
- fmt.Printf("Build time: %s\n", buildTime)
- os.Exit(0)
- }
-
- fmt.Printf("🚀 Starting Wormhole SOCKS5 Client %s\n", version)
- fmt.Printf("📄 Config: %s\n", *configPath)
- fmt.Printf("🔧 Mode: %s\n", *mode)
-
- // TODO: 实现完整的客户端逻辑
- cli := client.NewClient(*mode)
- if err := cli.Start(*configPath); err != nil {
- log.Fatalf("Client failed: %v", err)
- }
-}
-
-
-
package client
-
-import (
- "fmt"
- "os"
- "os/signal"
- "syscall"
-
- "github.com/azoic/wormhole-client/internal/config"
- "github.com/azoic/wormhole-client/internal/proxy"
- "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
-}
-
-func NewClient(mode string) *Client {
- return &Client{
- mode: mode,
- systemProxyMgr: system.NewSystemProxyManager(),
- }
-}
-
-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,
- )
-
- // 设置信号处理
- 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)
-
- server := c.socks5Proxy.CreateHTTPProxy(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)
-
- return server.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代理服务器
- server := c.socks5Proxy.CreateHTTPProxy(c.config.Proxy.LocalPort)
- go func() {
- if err := server.ListenAndServe(); err != nil {
- logger.Error("HTTP proxy server failed: %v", err)
- }
- }()
-
- // 设置系统代理
- httpProxy := fmt.Sprintf("127.0.0.1:%d", c.config.Proxy.LocalPort)
- httpsProxy := 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: Set HTTP/HTTPS proxy to %s", httpProxy)
- } 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)
- 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("🎉 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)
- }
-
- // 保持运行
- select {}
-}
-
-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.Error("❌ Transparent proxy mode is not yet implemented")
-
- return fmt.Errorf("transparent proxy mode not implemented")
-}
-
-func (c *Client) setupSignalHandler() {
- sigChan := make(chan os.Signal, 1)
- signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
-
- go func() {
- <-sigChan
- logger.Info("🛑 Shutting down...")
- c.cleanup()
- os.Exit(0)
- }()
-}
-
-func (c *Client) cleanup() {
- // 恢复系统代理设置
- if c.systemProxyMgr != nil {
- if err := c.systemProxyMgr.RestoreProxy(); err != nil {
- logger.Error("Failed to restore system proxy: %v", err)
- }
- }
-
- // 停止DNS代理
- if c.dnsProxy != nil {
- if err := c.dnsProxy.Stop(); err != nil {
- logger.Error("Failed to stop DNS proxy: %v", err)
- }
- }
-
- logger.Info("✅ Cleanup completed")
-}
-
-
-
package config
-
-import (
- "fmt"
- "io/ioutil"
- "time"
-
- "gopkg.in/yaml.v3"
-)
-
-// Config 客户端配置结构
-type Config struct {
- ServiceType string `yaml:"serviceType"`
- Server Server `yaml:"server"`
- Proxy Proxy `yaml:"proxy"`
- GlobalProxy GlobalProxy `yaml:"globalProxy"`
- TransparentProxy TransparentProxy `yaml:"transparentProxy"`
- LogLevel string `yaml:"logLevel"`
- Timeout time.Duration `yaml:"timeout"`
-}
-
-// Server SOCKS5服务器配置
-type Server struct {
- Address string `yaml:"address"`
- Port int `yaml:"port"`
- Username string `yaml:"username"`
- Password string `yaml:"password"`
-}
-
-// Proxy 代理模式配置
-type Proxy struct {
- Mode string `yaml:"mode"`
- LocalPort int `yaml:"localPort"`
-}
-
-// GlobalProxy 全局代理配置
-type GlobalProxy struct {
- Enabled bool `yaml:"enabled"`
- DNSProxy bool `yaml:"dnsProxy"`
- DNSPort int `yaml:"dnsPort"`
- Routing Routing `yaml:"routing"`
-}
-
-// TransparentProxy 透明代理配置
-type TransparentProxy struct {
- Enabled bool `yaml:"enabled"`
- Port int `yaml:"port"`
- DNSPort int `yaml:"dnsPort"`
- ModifyDNS bool `yaml:"modifyDNS"`
- ModifyRoute bool `yaml:"modifyRoute"`
-}
-
-// Routing 路由规则配置
-type Routing struct {
- BypassLocal bool `yaml:"bypassLocal"`
- BypassPrivate bool `yaml:"bypassPrivate"`
- BypassDomains []string `yaml:"bypassDomains"`
- ForceDomains []string `yaml:"forceDomains"`
-}
-
-// LoadConfig 从文件加载配置
-func LoadConfig(configPath string) (*Config, error) {
- data, err := ioutil.ReadFile(configPath)
- if err != nil {
- return nil, fmt.Errorf("failed to read config file: %v", err)
- }
-
- var config Config
- if err := yaml.Unmarshal(data, &config); err != nil {
- return nil, fmt.Errorf("failed to parse config file: %v", err)
- }
-
- // 设置默认值
- if config.LogLevel == "" {
- config.LogLevel = "info"
- }
- if config.Timeout == 0 {
- config.Timeout = 30 * time.Second
- }
- if config.Proxy.LocalPort == 0 {
- config.Proxy.LocalPort = 8080
- }
-
- return &config, nil
-}
-
-// GetServerAddr 获取服务器地址
-func (c *Config) GetServerAddr() string {
- return fmt.Sprintf("%s:%d", c.Server.Address, c.Server.Port)
-}
-
-// Validate 验证配置
-func (c *Config) Validate() error {
- if c.Server.Address == "" {
- return fmt.Errorf("server address is required")
- }
- if c.Server.Port <= 0 || c.Server.Port > 65535 {
- return fmt.Errorf("invalid server port: %d", c.Server.Port)
- }
-
- validModes := map[string]bool{"http": true, "global": true, "transparent": true}
- if !validModes[c.Proxy.Mode] {
- return fmt.Errorf("invalid proxy mode: %s", c.Proxy.Mode)
- }
-
- return nil
-}
-
-
-
package proxy
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net"
- "net/http"
- "strconv"
- "sync"
- "time"
-
- "github.com/azoic/wormhole-client/pkg/logger"
-)
-
-// SOCKS5Proxy SOCKS5代理客户端
-type SOCKS5Proxy struct {
- serverAddr string
- username string
- password string
- timeout time.Duration
- connPool *connectionPool
- stats *ProxyStats
-}
-
-// connectionPool 连接池
-type connectionPool struct {
- connections chan net.Conn
- maxSize int
- mutex sync.Mutex
-}
-
-// NewSOCKS5Proxy 创建SOCKS5代理客户端
-func NewSOCKS5Proxy(serverAddr, username, password string, timeout time.Duration) *SOCKS5Proxy {
- return &SOCKS5Proxy{
- serverAddr: serverAddr,
- username: username,
- password: password,
- timeout: timeout,
- connPool: &connectionPool{
- connections: make(chan net.Conn, 10),
- maxSize: 10,
- },
- stats: NewProxyStats(),
- }
-}
-
-// CreateHTTPProxy 创建HTTP代理服务器
-func (p *SOCKS5Proxy) CreateHTTPProxy(localPort int) *http.Server {
- proxyHandler := &httpProxyHandler{
- socks5Proxy: p,
- }
-
- // 创建ServeMux来处理不同的路径
- mux := http.NewServeMux()
- mux.Handle("/", proxyHandler)
- mux.HandleFunc("/stats", p.handleStats)
- mux.HandleFunc("/health", p.handleHealth)
-
- server := &http.Server{
- Addr: fmt.Sprintf(":%d", localPort),
- Handler: mux,
- ReadTimeout: 30 * time.Second,
- WriteTimeout: 30 * time.Second,
- IdleTimeout: 120 * time.Second,
- MaxHeaderBytes: 1 << 20, // 1MB
- }
-
- return server
-}
-
-// httpProxyHandler HTTP代理处理器
-type httpProxyHandler struct {
- socks5Proxy *SOCKS5Proxy
-}
-
-func (h *httpProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // 统计连接
- h.socks5Proxy.stats.IncrementConnections()
- defer h.socks5Proxy.stats.DecrementActiveConnections()
-
- logger.Debug("Processing request: %s %s from %s", r.Method, r.URL.String(), r.RemoteAddr)
-
- 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) {
- ctx, cancel := context.WithTimeout(r.Context(), h.socks5Proxy.timeout)
- defer cancel()
-
- destConn, err := h.socks5Proxy.DialTCPWithContext(ctx, r.Host)
- if err != nil {
- h.socks5Proxy.stats.IncrementFailedRequests()
- h.socks5Proxy.stats.IncrementSOCKS5Error("connection_failed")
- logger.Error("Failed to connect via SOCKS5 to %s: %v", r.Host, err)
- 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 {
- h.socks5Proxy.stats.IncrementFailedRequests()
- logger.Error("Hijacking not supported")
- http.Error(w, "Internal Server Error", http.StatusInternalServerError)
- return
- }
-
- clientConn, _, err := hijacker.Hijack()
- if err != nil {
- h.socks5Proxy.stats.IncrementFailedRequests()
- logger.Error("Failed to hijack connection: %v", err)
- return
- }
- defer clientConn.Close()
-
- h.socks5Proxy.stats.IncrementSuccessfulRequests()
- logger.Debug("Established HTTPS tunnel to %s", r.Host)
-
- // 双向数据转发
- var wg sync.WaitGroup
- wg.Add(2)
-
- go func() {
- defer wg.Done()
- written := h.copyData(clientConn, destConn, "client->server")
- h.socks5Proxy.stats.AddBytesTransferred(written, 0)
- }()
-
- go func() {
- defer wg.Done()
- written := h.copyData(destConn, clientConn, "server->client")
- h.socks5Proxy.stats.AddBytesTransferred(0, written)
- }()
-
- wg.Wait()
- logger.Debug("HTTPS tunnel to %s closed", r.Host)
-}
-
-// handleHTTPProxy 处理HTTP代理请求
-func (h *httpProxyHandler) handleHTTPProxy(w http.ResponseWriter, r *http.Request) {
- ctx, cancel := context.WithTimeout(r.Context(), h.socks5Proxy.timeout)
- defer cancel()
-
- // 确保URL包含Host
- if r.URL.Host == "" {
- r.URL.Host = r.Host
- }
- if r.URL.Scheme == "" {
- r.URL.Scheme = "http"
- }
-
- // 通过SOCKS5连接到目标服务器
- destConn, err := h.socks5Proxy.DialTCPWithContext(ctx, r.Host)
- if err != nil {
- h.socks5Proxy.stats.IncrementFailedRequests()
- h.socks5Proxy.stats.IncrementSOCKS5Error("connection_failed")
- logger.Error("Failed to connect via SOCKS5 to %s: %v", r.Host, err)
- http.Error(w, "Bad Gateway", http.StatusBadGateway)
- return
- }
- defer destConn.Close()
-
- // 发送HTTP请求
- if err := r.Write(destConn); err != nil {
- h.socks5Proxy.stats.IncrementFailedRequests()
- logger.Error("Failed to write request to %s: %v", r.Host, err)
- http.Error(w, "Bad Gateway", http.StatusBadGateway)
- return
- }
-
- // 设置响应头
- w.Header().Set("Via", "1.1 wormhole-proxy")
-
- // 使用自定义ResponseWriter来统计字节数
- statsWriter := &statsResponseWriter{
- ResponseWriter: w,
- stats: h.socks5Proxy.stats,
- }
-
- // 读取响应并返回给客户端
- written, err := io.Copy(statsWriter, destConn)
- if err != nil {
- h.socks5Proxy.stats.IncrementFailedRequests()
- logger.Error("Failed to copy response from %s: %v", r.Host, err)
- return
- }
-
- h.socks5Proxy.stats.IncrementSuccessfulRequests()
- h.socks5Proxy.stats.AddBytesTransferred(0, written)
- logger.Debug("HTTP request to %s completed, %d bytes", r.Host, written)
-}
-
-// statsResponseWriter 带统计功能的ResponseWriter
-type statsResponseWriter struct {
- http.ResponseWriter
- stats *ProxyStats
-}
-
-func (w *statsResponseWriter) Write(data []byte) (int, error) {
- n, err := w.ResponseWriter.Write(data)
- if n > 0 {
- w.stats.AddBytesTransferred(int64(n), 0)
- }
- return n, err
-}
-
-// copyData 数据复制,带方向标识和字节统计
-func (h *httpProxyHandler) copyData(dst, src net.Conn, direction string) int64 {
- defer dst.Close()
- defer src.Close()
-
- written, err := io.Copy(dst, src)
- if err != nil {
- logger.Debug("Copy %s finished with error: %v, bytes: %d", direction, err, written)
- } else {
- logger.Debug("Copy %s finished successfully, bytes: %d", direction, written)
- }
-
- return written
-}
-
-// handleStats 处理统计信息请求
-func (p *SOCKS5Proxy) handleStats(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- stats := p.stats.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
- }
-}
-
-// handleHealth 处理健康检查请求
-func (p *SOCKS5Proxy) handleHealth(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- stats := p.stats.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
- }
-}
-
-// GetStats 获取代理统计信息
-func (p *SOCKS5Proxy) GetStats() ProxyStatsSnapshot {
- return p.stats.GetStats()
-}
-
-// DialTCP 通过SOCKS5连接到目标地址
-func (p *SOCKS5Proxy) DialTCP(address string) (net.Conn, error) {
- return p.DialTCPWithContext(context.Background(), address)
-}
-
-// DialTCPWithContext 通过SOCKS5连接到目标地址(带上下文)
-func (p *SOCKS5Proxy) DialTCPWithContext(ctx context.Context, address string) (net.Conn, error) {
- // 连接到SOCKS5代理服务器
- dialer := &net.Dialer{
- Timeout: p.timeout,
- }
-
- conn, err := dialer.DialContext(ctx, "tcp", p.serverAddr)
- if err != nil {
- return nil, fmt.Errorf("failed to connect to SOCKS5 server: %v", err)
- }
-
- // 设置连接超时
- deadline, ok := ctx.Deadline()
- if ok {
- conn.SetDeadline(deadline)
- }
-
- // 执行SOCKS5握手
- if err := p.performSOCKS5Handshake(conn, address); err != nil {
- conn.Close()
- return nil, fmt.Errorf("SOCKS5 handshake failed: %v", err)
- }
-
- // 清除deadline,让连接正常使用
- conn.SetDeadline(time.Time{})
-
- logger.Debug("Successfully connected to %s via SOCKS5 proxy", address)
- return conn, nil
-}
-
-// performSOCKS5Handshake 执行SOCKS5握手协议
-func (p *SOCKS5Proxy) performSOCKS5Handshake(conn net.Conn, targetAddr string) error {
- // 设置握手超时
- deadline := time.Now().Add(p.timeout)
- conn.SetDeadline(deadline)
-
- // 第一步:发送认证方法选择
- authMethods := []byte{0x05, 0x02, 0x00, 0x02} // 版本5,2个方法,无认证+用户名密码认证
- if _, err := conn.Write(authMethods); err != nil {
- return fmt.Errorf("failed to send auth methods: %v", err)
- }
-
- // 读取服务器响应
- response := make([]byte, 2)
- if _, err := io.ReadFull(conn, response); err != nil {
- return fmt.Errorf("failed to read auth response: %v", err)
- }
-
- if response[0] != 0x05 {
- return fmt.Errorf("invalid SOCKS version: %d", response[0])
- }
-
- // 第二步:处理认证
- switch response[1] {
- case 0x00: // 无认证
- logger.Debug("SOCKS5 server requires no authentication")
- case 0x02: // 用户名密码认证
- if err := p.performUserPassAuth(conn); err != nil {
- return fmt.Errorf("user/pass authentication failed: %v", err)
- }
- case 0xFF: // 无可接受的认证方法
- return fmt.Errorf("no acceptable authentication methods")
- default:
- return fmt.Errorf("unsupported authentication method: %d", response[1])
- }
-
- // 第三步:发送连接请求
- connectReq, err := p.buildConnectRequest(targetAddr)
- if err != nil {
- return fmt.Errorf("failed to build connect request: %v", err)
- }
-
- if _, err := conn.Write(connectReq); err != nil {
- return fmt.Errorf("failed to send connect request: %v", err)
- }
-
- // 第四步:读取连接响应
- return p.readConnectResponse(conn)
-}
-
-// buildConnectRequest 构建连接请求
-func (p *SOCKS5Proxy) buildConnectRequest(targetAddr string) ([]byte, error) {
- host, portStr, err := net.SplitHostPort(targetAddr)
- if err != nil {
- return nil, fmt.Errorf("invalid target address: %v", err)
- }
-
- // 解析端口号
- portNum, err := parsePort(portStr)
- if err != nil {
- return nil, fmt.Errorf("invalid port: %v", err)
- }
-
- var connectReq []byte
-
- // 检测地址类型并构建请求
- if ip := net.ParseIP(host); ip != nil {
- if ip4 := ip.To4(); ip4 != nil {
- // IPv4地址
- connectReq = []byte{0x05, 0x01, 0x00, 0x01}
- connectReq = append(connectReq, ip4...)
- } else if ip6 := ip.To16(); ip6 != nil {
- // IPv6地址
- connectReq = []byte{0x05, 0x01, 0x00, 0x04}
- connectReq = append(connectReq, ip6...)
- }
- } else {
- // 域名
- if len(host) > 255 {
- return nil, fmt.Errorf("domain name too long: %d", len(host))
- }
- connectReq = []byte{0x05, 0x01, 0x00, 0x03}
- connectReq = append(connectReq, byte(len(host)))
- connectReq = append(connectReq, []byte(host)...)
- }
-
- // 添加端口
- connectReq = append(connectReq, byte(portNum>>8), byte(portNum&0xFF))
-
- return connectReq, nil
-}
-
-// readConnectResponse 读取连接响应
-func (p *SOCKS5Proxy) readConnectResponse(conn net.Conn) error {
- // 读取响应头部
- header := make([]byte, 4)
- if _, err := io.ReadFull(conn, header); err != nil {
- return fmt.Errorf("failed to read connect response header: %v", err)
- }
-
- if header[0] != 0x05 {
- return fmt.Errorf("invalid SOCKS version in response: %d", header[0])
- }
-
- if header[1] != 0x00 {
- return fmt.Errorf("connection failed, status: %d (%s)", header[1], getSOCKS5ErrorMessage(header[1]))
- }
-
- // 读取绑定地址和端口
- addrType := header[3]
- switch addrType {
- case 0x01: // IPv4
- skipBytes := make([]byte, 6) // 4字节IP + 2字节端口
- _, err := io.ReadFull(conn, skipBytes)
- return err
- case 0x03: // 域名
- lenByte := make([]byte, 1)
- if _, err := io.ReadFull(conn, lenByte); err != nil {
- return err
- }
- skipBytes := make([]byte, int(lenByte[0])+2) // 域名长度 + 2字节端口
- _, err := io.ReadFull(conn, skipBytes)
- return err
- case 0x04: // IPv6
- skipBytes := make([]byte, 18) // 16字节IP + 2字节端口
- _, err := io.ReadFull(conn, skipBytes)
- return err
- default:
- return fmt.Errorf("unsupported address type: %d", addrType)
- }
-}
-
-// performUserPassAuth 执行用户名密码认证
-func (p *SOCKS5Proxy) performUserPassAuth(conn net.Conn) error {
- // 发送用户名密码
- authData := []byte{0x01} // 子协议版本
- authData = append(authData, byte(len(p.username)))
- authData = append(authData, []byte(p.username)...)
- authData = append(authData, byte(len(p.password)))
- authData = append(authData, []byte(p.password)...)
-
- if _, err := conn.Write(authData); err != nil {
- return fmt.Errorf("failed to send credentials: %v", err)
- }
-
- // 读取认证结果
- authResult := make([]byte, 2)
- if _, err := io.ReadFull(conn, authResult); err != nil {
- return fmt.Errorf("failed to read auth result: %v", err)
- }
-
- if authResult[0] != 0x01 {
- return fmt.Errorf("invalid auth response version: %d", authResult[0])
- }
-
- if authResult[1] != 0x00 {
- return fmt.Errorf("authentication failed")
- }
-
- logger.Debug("SOCKS5 authentication successful")
- return nil
-}
-
-// getSOCKS5ErrorMessage 获取SOCKS5错误消息
-func getSOCKS5ErrorMessage(code byte) string {
- switch code {
- case 0x01:
- return "general SOCKS server failure"
- case 0x02:
- return "connection not allowed by ruleset"
- case 0x03:
- return "network unreachable"
- case 0x04:
- return "host unreachable"
- case 0x05:
- return "connection refused"
- case 0x06:
- return "TTL expired"
- case 0x07:
- return "command not supported"
- case 0x08:
- return "address type not supported"
- default:
- return "unknown error"
- }
-}
-
-// parsePort 解析端口号
-func parsePort(portStr string) (int, error) {
- if portStr == "" {
- return 80, nil // 默认HTTP端口
- }
-
- port, err := strconv.Atoi(portStr)
- if err != nil {
- return 0, fmt.Errorf("invalid port format: %s", portStr)
- }
-
- if port < 1 || port > 65535 {
- return 0, fmt.Errorf("port out of range: %d", port)
- }
-
- return port, nil
-}
-
-
-
package proxy
-
-import (
- "sync"
- "sync/atomic"
- "time"
-)
-
-// ProxyStats 代理统计信息
-type ProxyStats struct {
- StartTime time.Time
- TotalConnections int64
- ActiveConnections int64
- SuccessfulRequests int64
- FailedRequests int64
- BytesSent int64
- BytesReceived int64
- SOCKS5Errors map[string]int64
- mutex sync.RWMutex
-}
-
-// NewProxyStats 创建新的统计实例
-func NewProxyStats() *ProxyStats {
- return &ProxyStats{
- StartTime: time.Now(),
- SOCKS5Errors: make(map[string]int64),
- }
-}
-
-// IncrementConnections 增加连接计数
-func (s *ProxyStats) IncrementConnections() {
- atomic.AddInt64(&s.TotalConnections, 1)
- atomic.AddInt64(&s.ActiveConnections, 1)
-}
-
-// DecrementActiveConnections 减少活跃连接计数
-func (s *ProxyStats) DecrementActiveConnections() {
- atomic.AddInt64(&s.ActiveConnections, -1)
-}
-
-// IncrementSuccessfulRequests 增加成功请求计数
-func (s *ProxyStats) IncrementSuccessfulRequests() {
- atomic.AddInt64(&s.SuccessfulRequests, 1)
-}
-
-// IncrementFailedRequests 增加失败请求计数
-func (s *ProxyStats) IncrementFailedRequests() {
- atomic.AddInt64(&s.FailedRequests, 1)
-}
-
-// AddBytesTransferred 添加传输字节数
-func (s *ProxyStats) AddBytesTransferred(sent, received int64) {
- atomic.AddInt64(&s.BytesSent, sent)
- atomic.AddInt64(&s.BytesReceived, received)
-}
-
-// IncrementSOCKS5Error 增加SOCKS5错误计数
-func (s *ProxyStats) IncrementSOCKS5Error(errorType string) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.SOCKS5Errors[errorType]++
-}
-
-// GetStats 获取统计快照
-func (s *ProxyStats) GetStats() ProxyStatsSnapshot {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
-
- errors := make(map[string]int64)
- for k, v := range s.SOCKS5Errors {
- errors[k] = v
- }
-
- return ProxyStatsSnapshot{
- StartTime: s.StartTime,
- Uptime: time.Since(s.StartTime),
- TotalConnections: atomic.LoadInt64(&s.TotalConnections),
- ActiveConnections: atomic.LoadInt64(&s.ActiveConnections),
- SuccessfulRequests: atomic.LoadInt64(&s.SuccessfulRequests),
- FailedRequests: atomic.LoadInt64(&s.FailedRequests),
- BytesSent: atomic.LoadInt64(&s.BytesSent),
- BytesReceived: atomic.LoadInt64(&s.BytesReceived),
- SOCKS5Errors: errors,
- }
-}
-
-// ProxyStatsSnapshot 统计快照
-type ProxyStatsSnapshot struct {
- StartTime time.Time `json:"start_time"`
- Uptime time.Duration `json:"uptime"`
- TotalConnections int64 `json:"total_connections"`
- ActiveConnections int64 `json:"active_connections"`
- SuccessfulRequests int64 `json:"successful_requests"`
- FailedRequests int64 `json:"failed_requests"`
- BytesSent int64 `json:"bytes_sent"`
- BytesReceived int64 `json:"bytes_received"`
- SOCKS5Errors map[string]int64 `json:"socks5_errors"`
-}
-
-// GetSuccessRate 获取成功率
-func (s *ProxyStatsSnapshot) GetSuccessRate() float64 {
- total := s.SuccessfulRequests + s.FailedRequests
- if total == 0 {
- return 0
- }
- return float64(s.SuccessfulRequests) / float64(total) * 100
-}
-
-// GetTotalBytes 获取总传输字节数
-func (s *ProxyStatsSnapshot) GetTotalBytes() int64 {
- return s.BytesSent + s.BytesReceived
-}
-
-// GetAverageConnectionsPerHour 获取每小时平均连接数
-func (s *ProxyStatsSnapshot) GetAverageConnectionsPerHour() float64 {
- hours := s.Uptime.Hours()
- if hours == 0 {
- return 0
- }
- return float64(s.TotalConnections) / hours
-}
-
-
-
package system
-
-import (
- "fmt"
- "os/exec"
- "runtime"
- "strings"
-
- "github.com/azoic/wormhole-client/pkg/logger"
-)
-
-// SystemProxyManager 系统代理管理器
-type SystemProxyManager struct {
- originalSettings map[string]string
-}
-
-// NewSystemProxyManager 创建系统代理管理器
-func NewSystemProxyManager() *SystemProxyManager {
- return &SystemProxyManager{
- originalSettings: make(map[string]string),
- }
-}
-
-// SetGlobalProxy 设置全局代理
-func (s *SystemProxyManager) SetGlobalProxy(httpProxy, httpsProxy, socksProxy string) error {
- switch runtime.GOOS {
- case "darwin":
- return s.setMacOSProxy(httpProxy, httpsProxy, socksProxy)
- case "windows":
- return s.setWindowsProxy(httpProxy, httpsProxy, socksProxy)
- case "linux":
- return s.setLinuxProxy(httpProxy, httpsProxy, socksProxy)
- default:
- return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
- }
-}
-
-// RestoreProxy 恢复原始代理设置
-func (s *SystemProxyManager) RestoreProxy() error {
- switch runtime.GOOS {
- case "darwin":
- return s.restoreMacOSProxy()
- case "windows":
- return s.restoreWindowsProxy()
- case "linux":
- return s.restoreLinuxProxy()
- default:
- return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
- }
-}
-
-// macOS 代理设置
-func (s *SystemProxyManager) setMacOSProxy(httpProxy, httpsProxy, socksProxy string) error {
- logger.Info("Setting macOS system proxy...")
-
- // 获取网络服务名称
- networkService, err := s.getMacOSNetworkService()
- if err != nil {
- return fmt.Errorf("failed to get network service: %v", err)
- }
-
- // 保存原始设置
- if err := s.saveMacOSOriginalSettings(networkService); err != nil {
- logger.Warn("Failed to save original proxy settings: %v", err)
- }
-
- // 设置HTTP代理
- if httpProxy != "" {
- if err := s.runCommand("networksetup", "-setwebproxy", networkService,
- s.parseProxyHost(httpProxy), s.parseProxyPort(httpProxy)); err != nil {
- return fmt.Errorf("failed to set HTTP proxy: %v", err)
- }
-
- if err := s.runCommand("networksetup", "-setwebproxystate", networkService, "on"); err != nil {
- return fmt.Errorf("failed to enable HTTP proxy: %v", err)
- }
- }
-
- // 设置HTTPS代理
- if httpsProxy != "" {
- if err := s.runCommand("networksetup", "-setsecurewebproxy", networkService,
- s.parseProxyHost(httpsProxy), s.parseProxyPort(httpsProxy)); err != nil {
- return fmt.Errorf("failed to set HTTPS proxy: %v", err)
- }
-
- if err := s.runCommand("networksetup", "-setsecurewebproxystate", networkService, "on"); err != nil {
- return fmt.Errorf("failed to enable HTTPS proxy: %v", err)
- }
- }
-
- // 设置SOCKS代理
- if socksProxy != "" {
- if err := s.runCommand("networksetup", "-setsocksfirewallproxy", networkService,
- s.parseProxyHost(socksProxy), s.parseProxyPort(socksProxy)); err != nil {
- return fmt.Errorf("failed to set SOCKS proxy: %v", err)
- }
-
- if err := s.runCommand("networksetup", "-setsocksfirewallproxystate", networkService, "on"); err != nil {
- return fmt.Errorf("failed to enable SOCKS proxy: %v", err)
- }
- }
-
- logger.Info("macOS system proxy configured successfully")
- return nil
-}
-
-// 获取macOS网络服务名称
-func (s *SystemProxyManager) getMacOSNetworkService() (string, error) {
- output, err := exec.Command("networksetup", "-listallnetworkservices").Output()
- if err != nil {
- return "", err
- }
-
- lines := strings.Split(string(output), "\n")
- for _, line := range lines {
- line = strings.TrimSpace(line)
- if line != "" && !strings.HasPrefix(line, "*") && !strings.Contains(line, "An asterisk") {
- // 优先选择Wi-Fi,否则选择第一个可用的服务
- if strings.Contains(strings.ToLower(line), "wi-fi") || strings.Contains(strings.ToLower(line), "wifi") {
- return line, nil
- }
- }
- }
-
- // 如果没找到Wi-Fi,返回第一个可用的服务
- for _, line := range lines {
- line = strings.TrimSpace(line)
- if line != "" && !strings.HasPrefix(line, "*") && !strings.Contains(line, "An asterisk") {
- return line, nil
- }
- }
-
- return "", fmt.Errorf("no network service found")
-}
-
-// 保存macOS原始代理设置
-func (s *SystemProxyManager) saveMacOSOriginalSettings(networkService string) error {
- // 保存HTTP代理状态
- if output, err := exec.Command("networksetup", "-getwebproxy", networkService).Output(); err == nil {
- s.originalSettings["http_proxy"] = string(output)
- }
-
- // 保存HTTPS代理状态
- if output, err := exec.Command("networksetup", "-getsecurewebproxy", networkService).Output(); err == nil {
- s.originalSettings["https_proxy"] = string(output)
- }
-
- // 保存SOCKS代理状态
- if output, err := exec.Command("networksetup", "-getsocksfirewallproxy", networkService).Output(); err == nil {
- s.originalSettings["socks_proxy"] = string(output)
- }
-
- s.originalSettings["network_service"] = networkService
- return nil
-}
-
-// 恢复macOS代理设置
-func (s *SystemProxyManager) restoreMacOSProxy() error {
- networkService, exists := s.originalSettings["network_service"]
- if !exists {
- return fmt.Errorf("no network service information saved")
- }
-
- logger.Info("Restoring macOS system proxy...")
-
- // 关闭所有代理
- s.runCommand("networksetup", "-setwebproxystate", networkService, "off")
- s.runCommand("networksetup", "-setsecurewebproxystate", networkService, "off")
- s.runCommand("networksetup", "-setsocksfirewallproxystate", networkService, "off")
-
- logger.Info("macOS system proxy restored")
- return nil
-}
-
-// Windows 代理设置(简化实现)
-func (s *SystemProxyManager) setWindowsProxy(httpProxy, httpsProxy, socksProxy string) error {
- logger.Warn("Windows proxy setting not fully implemented")
- return fmt.Errorf("Windows proxy setting not implemented yet")
-}
-
-func (s *SystemProxyManager) restoreWindowsProxy() error {
- logger.Warn("Windows proxy restoration not fully implemented")
- return nil
-}
-
-// Linux 代理设置(简化实现)
-func (s *SystemProxyManager) setLinuxProxy(httpProxy, httpsProxy, socksProxy string) error {
- logger.Warn("Linux proxy setting not fully implemented")
- return fmt.Errorf("Linux proxy setting not implemented yet")
-}
-
-func (s *SystemProxyManager) restoreLinuxProxy() error {
- logger.Warn("Linux proxy restoration not fully implemented")
- return nil
-}
-
-// 辅助函数
-func (s *SystemProxyManager) runCommand(name string, args ...string) error {
- cmd := exec.Command(name, args...)
- output, err := cmd.CombinedOutput()
- if err != nil {
- logger.Error("Command failed: %s %v, output: %s", name, args, string(output))
- return err
- }
- return nil
-}
-
-func (s *SystemProxyManager) parseProxyHost(proxy string) string {
- // 简单解析,格式: host:port
- parts := strings.Split(proxy, ":")
- if len(parts) >= 1 {
- return parts[0]
- }
- return proxy
-}
-
-func (s *SystemProxyManager) parseProxyPort(proxy string) string {
- // 简单解析,格式: host:port
- parts := strings.Split(proxy, ":")
- if len(parts) >= 2 {
- return parts[1]
- }
- return "8080" // 默认端口
-}
-
-
-
package dns
-
-import (
- "fmt"
- "net"
- "strings"
- "sync"
- "time"
-
- "github.com/azoic/wormhole-client/pkg/logger"
-)
-
-// DNSProxy DNS代理服务器
-type DNSProxy struct {
- upstreamDNS string
- localPort int
- server *net.UDPConn
- cache *dnsCache
- running bool
- mutex sync.RWMutex
-}
-
-// dnsCache DNS缓存
-type dnsCache struct {
- entries map[string]*cacheEntry
- mutex sync.RWMutex
-}
-
-type cacheEntry struct {
- response []byte
- expiry time.Time
-}
-
-// NewDNSProxy 创建DNS代理
-func NewDNSProxy(upstreamDNS string, localPort int) *DNSProxy {
- return &DNSProxy{
- upstreamDNS: upstreamDNS,
- localPort: localPort,
- cache: &dnsCache{
- entries: make(map[string]*cacheEntry),
- },
- }
-}
-
-// Start 启动DNS代理服务器
-func (d *DNSProxy) Start() error {
- d.mutex.Lock()
- defer d.mutex.Unlock()
-
- if d.running {
- return fmt.Errorf("DNS proxy is already running")
- }
-
- addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", d.localPort))
- if err != nil {
- return fmt.Errorf("failed to resolve UDP address: %v", err)
- }
-
- d.server, err = net.ListenUDP("udp", addr)
- if err != nil {
- return fmt.Errorf("failed to start DNS proxy server: %v", err)
- }
-
- d.running = true
- logger.Info("DNS proxy started on port %d", d.localPort)
-
- // 启动清理缓存的goroutine
- go d.cleanupCache()
-
- // 处理DNS请求
- go d.handleRequests()
-
- return nil
-}
-
-// Stop 停止DNS代理服务器
-func (d *DNSProxy) Stop() error {
- d.mutex.Lock()
- defer d.mutex.Unlock()
-
- if !d.running {
- return nil
- }
-
- d.running = false
- if d.server != nil {
- d.server.Close()
- }
-
- logger.Info("DNS proxy stopped")
- return nil
-}
-
-// handleRequests 处理DNS请求
-func (d *DNSProxy) handleRequests() {
- buffer := make([]byte, 512) // DNS消息最大512字节(UDP)
-
- for d.isRunning() {
- n, clientAddr, err := d.server.ReadFromUDP(buffer)
- if err != nil {
- if d.isRunning() {
- logger.Error("Failed to read DNS request: %v", err)
- }
- continue
- }
-
- go d.processRequest(buffer[:n], clientAddr)
- }
-}
-
-// processRequest 处理单个DNS请求
-func (d *DNSProxy) processRequest(request []byte, clientAddr *net.UDPAddr) {
- // 简单的DNS请求解析
- domain := d.extractDomain(request)
- logger.Debug("DNS request for domain: %s", domain)
-
- // 检查缓存
- if cachedResponse := d.getFromCache(domain); cachedResponse != nil {
- logger.Debug("Serving %s from cache", domain)
- d.server.WriteToUDP(cachedResponse, clientAddr)
- return
- }
-
- // 转发到上游DNS服务器
- response, err := d.forwardToUpstream(request)
- if err != nil {
- logger.Error("Failed to forward DNS request: %v", err)
- return
- }
-
- // 缓存响应
- d.addToCache(domain, response)
-
- // 返回响应给客户端
- d.server.WriteToUDP(response, clientAddr)
-}
-
-// extractDomain 从DNS请求中提取域名(简化实现)
-func (d *DNSProxy) extractDomain(request []byte) string {
- if len(request) < 12 {
- return ""
- }
-
- // 跳过DNS头部(12字节)
- offset := 12
- var domain strings.Builder
-
- for offset < len(request) {
- length := int(request[offset])
- if length == 0 {
- break
- }
-
- offset++
- if offset+length > len(request) {
- break
- }
-
- if domain.Len() > 0 {
- domain.WriteByte('.')
- }
- domain.Write(request[offset : offset+length])
- offset += length
- }
-
- return domain.String()
-}
-
-// forwardToUpstream 转发DNS请求到上游服务器
-func (d *DNSProxy) forwardToUpstream(request []byte) ([]byte, error) {
- conn, err := net.Dial("udp", d.upstreamDNS)
- if err != nil {
- return nil, fmt.Errorf("failed to connect to upstream DNS: %v", err)
- }
- defer conn.Close()
-
- // 设置超时
- conn.SetDeadline(time.Now().Add(5 * time.Second))
-
- // 发送请求
- if _, err := conn.Write(request); err != nil {
- return nil, fmt.Errorf("failed to send DNS request: %v", err)
- }
-
- // 读取响应
- response := make([]byte, 512)
- n, err := conn.Read(response)
- if err != nil {
- return nil, fmt.Errorf("failed to read DNS response: %v", err)
- }
-
- return response[:n], nil
-}
-
-// getFromCache 从缓存获取DNS响应
-func (d *DNSProxy) getFromCache(domain string) []byte {
- d.cache.mutex.RLock()
- defer d.cache.mutex.RUnlock()
-
- entry, exists := d.cache.entries[domain]
- if !exists || time.Now().After(entry.expiry) {
- return nil
- }
-
- return entry.response
-}
-
-// addToCache 添加DNS响应到缓存
-func (d *DNSProxy) addToCache(domain string, response []byte) {
- d.cache.mutex.Lock()
- defer d.cache.mutex.Unlock()
-
- // 设置缓存过期时间(5分钟)
- expiry := time.Now().Add(5 * time.Minute)
-
- d.cache.entries[domain] = &cacheEntry{
- response: make([]byte, len(response)),
- expiry: expiry,
- }
- copy(d.cache.entries[domain].response, response)
-}
-
-// cleanupCache 清理过期的缓存条目
-func (d *DNSProxy) cleanupCache() {
- ticker := time.NewTicker(1 * time.Minute)
- defer ticker.Stop()
-
- for {
- select {
- case <-ticker.C:
- if !d.isRunning() {
- return
- }
-
- d.cache.mutex.Lock()
- now := time.Now()
- for domain, entry := range d.cache.entries {
- if now.After(entry.expiry) {
- delete(d.cache.entries, domain)
- }
- }
- d.cache.mutex.Unlock()
- }
- }
-}
-
-// isRunning 检查DNS代理是否在运行
-func (d *DNSProxy) isRunning() bool {
- d.mutex.RLock()
- defer d.mutex.RUnlock()
- return d.running
-}
-
-
-
package logger
-
-import (
- "fmt"
- "log"
- "os"
- "strings"
- "time"
-)
-
-type LogLevel int
-
-const (
- DEBUG LogLevel = iota
- INFO
- WARN
- ERROR
-)
-
-type Logger struct {
- level LogLevel
- logger *log.Logger
-}
-
-var defaultLogger *Logger
-
-func init() {
- defaultLogger = New("INFO")
-}
-
-// New 创建新的日志记录器
-func New(levelStr string) *Logger {
- level := parseLogLevel(levelStr)
- logger := log.New(os.Stdout, "", 0)
-
- return &Logger{
- level: level,
- logger: logger,
- }
-}
-
-// SetLevel 设置日志级别
-func SetLevel(levelStr string) {
- defaultLogger.level = parseLogLevel(levelStr)
-}
-
-func parseLogLevel(levelStr string) LogLevel {
- switch strings.ToUpper(levelStr) {
- case "DEBUG":
- return DEBUG
- case "INFO":
- return INFO
- case "WARN", "WARNING":
- return WARN
- case "ERROR":
- return ERROR
- default:
- return INFO
- }
-}
-
-func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
- if level < l.level {
- return
- }
-
- levelStr := ""
- switch level {
- case DEBUG:
- levelStr = "DEBUG"
- case INFO:
- levelStr = "INFO"
- case WARN:
- levelStr = "WARN"
- case ERROR:
- levelStr = "ERROR"
- }
-
- timestamp := time.Now().Format("2006-01-02 15:04:05")
- message := fmt.Sprintf(format, args...)
- logLine := fmt.Sprintf("[%s] [%s] %s", timestamp, levelStr, message)
-
- l.logger.Println(logLine)
-}
-
-// Debug 调试日志
-func (l *Logger) Debug(format string, args ...interface{}) {
- l.log(DEBUG, format, args...)
-}
-
-// Info 信息日志
-func (l *Logger) Info(format string, args ...interface{}) {
- l.log(INFO, format, args...)
-}
-
-// Warn 警告日志
-func (l *Logger) Warn(format string, args ...interface{}) {
- l.log(WARN, format, args...)
-}
-
-// Error 错误日志
-func (l *Logger) Error(format string, args ...interface{}) {
- l.log(ERROR, format, args...)
-}
-
-// 全局日志函数
-func Debug(format string, args ...interface{}) {
- defaultLogger.Debug(format, args...)
-}
-
-func Info(format string, args ...interface{}) {
- defaultLogger.Info(format, args...)
-}
-
-func Warn(format string, args ...interface{}) {
- defaultLogger.Warn(format, args...)
-}
-
-func Error(format string, args ...interface{}) {
- defaultLogger.Error(format, args...)
-}
-
-
-
-
-
-
diff --git a/docs/BUILD_DMG.md b/docs/BUILD_DMG.md
new file mode 100644
index 0000000..24a152a
--- /dev/null
+++ b/docs/BUILD_DMG.md
@@ -0,0 +1,195 @@
+# macOS DMG 安装包构建指南
+
+本文档介绍如何为 Wormhole SOCKS5 Client 构建 macOS DMG 安装包。
+
+## 🔧 环境要求
+
+### 系统要求
+- **macOS 10.13+** (High Sierra 或更高版本)
+- **Xcode Command Line Tools** 或完整的 Xcode
+- **Go 1.19+**
+
+### 可选工具
+- **Python 3** + **Pillow (PIL)** - 用于生成自定义应用图标
+- **Git** - 用于版本控制
+
+## 🚀 快速开始
+
+### 一键构建
+```bash
+# 构建完整的 DMG 安装包
+make dmg
+
+# 或者使用完整命令
+make dmg-macos
+```
+
+### 分步构建
+```bash
+# 1. 构建 macOS 应用程序包
+./scripts/build-macos.sh
+
+# 2. 创建 DMG 安装包
+./scripts/create-dmg.sh
+
+# 3. 验证和测试
+open build/Wormhole-Client-v1.0.0.dmg
+```
+
+## 📁 构建产物
+
+构建成功后,将在 `build/` 目录下生成以下文件:
+
+```
+build/
+├── Wormhole Client.app/ # macOS 应用程序包
+│ ├── Contents/
+│ │ ├── Info.plist # 应用信息
+│ │ ├── MacOS/
+│ │ │ ├── wormhole-client # 二进制文件
+│ │ │ └── Wormhole Client # 启动脚本
+│ │ └── Resources/
+│ │ ├── configs/ # 配置文件
+│ │ └── app.icns # 应用图标
+│ └── ...
+└── Wormhole-Client-v1.0.0.dmg # 最终安装包
+```
+
+## 🎨 自定义图标
+
+### 使用自定义图标
+1. 准备一个 1024x1024 的 PNG 图片
+2. 将其保存为 `assets/icons/icon.png`
+3. 重新构建即可使用自定义图标
+
+### 自动生成图标
+如果没有自定义图标,构建脚本会自动:
+1. 尝试使用 Python PIL 生成简单图标
+2. 如果失败,使用系统默认应用图标
+
+## 📦 DMG 特性
+
+### 安装体验
+- **拖拽安装**: 用户只需将 app 拖到 Applications 文件夹
+- **自动布局**: 优化的窗口布局和图标位置
+- **说明文档**: 包含安装和使用说明
+
+### DMG 内容
+- `Wormhole Client.app` - 主应用程序
+- `Applications` - Applications 文件夹快捷方式
+- `README.txt` - 安装和使用说明
+
+## 🔍 构建脚本详解
+
+### build-macos.sh
+- 编译 Go 二进制文件 (amd64 架构)
+- 创建标准的 macOS 应用程序包结构
+- 复制配置文件和资源
+- 生成或复制应用图标
+- 创建 Info.plist 文件
+- 设置启动脚本
+
+### create-dmg.sh
+- 创建临时 DMG 工作目录
+- 复制应用程序和创建符号链接
+- 生成说明文档
+- 使用 AppleScript 美化 DMG 外观
+- 创建压缩的只读 DMG
+- 验证 DMG 完整性
+
+### build-dmg.sh
+- 一键执行完整构建流程
+- 环境检查和工具验证
+- 构建结果汇总和验证
+
+## 🛠️ 故障排除
+
+### 常见问题
+
+#### 1. hdiutil 权限错误
+```bash
+# 解决方案:使用 sudo 或给予磁盘访问权限
+sudo ./scripts/create-dmg.sh
+```
+
+#### 2. AppleScript 执行失败
+```bash
+# 检查系统完整性保护 (SIP)
+csrutil status
+
+# 在系统偏好设置中允许自动化权限
+# 系统偏好设置 > 安全性与隐私 > 隐私 > 自动化
+```
+
+#### 3. Go 编译失败
+```bash
+# 检查 Go 版本
+go version
+
+# 确保依赖已下载
+go mod download
+go mod tidy
+```
+
+### 调试模式
+```bash
+# 开启详细输出
+set -x
+./scripts/build-dmg.sh
+```
+
+## 📱 应用程序使用
+
+### 安装后使用
+1. 双击 DMG 文件挂载
+2. 拖拽 `Wormhole Client.app` 到 Applications
+3. 从 Applications 或 Launchpad 启动应用
+
+### 启动模式
+- **双击启动**: 自动以 HTTP 模式启动并打开 Web 界面
+- **终端启动**: 支持所有命令行参数
+ ```bash
+ # HTTP 模式
+ /Applications/Wormhole\ Client.app/Contents/MacOS/Wormhole\ Client -mode http
+
+ # 全局模式 (需要 sudo)
+ sudo /Applications/Wormhole\ Client.app/Contents/MacOS/Wormhole\ Client -mode global
+ ```
+
+### Web 界面
+- **管理界面**: http://127.0.0.1:8080/gui
+- **统计信息**: http://127.0.0.1:8080/stats
+- **健康检查**: http://127.0.0.1:8080/health
+
+## 🔒 代码签名 (可选)
+
+如果需要分发给其他用户,建议进行代码签名:
+
+```bash
+# 签名应用程序
+codesign --force --deep --sign "Developer ID Application: Your Name" \
+ "build/Wormhole Client.app"
+
+# 签名 DMG
+codesign --force --sign "Developer ID Application: Your Name" \
+ "build/Wormhole-Client-v1.0.0.dmg"
+
+# 验证签名
+codesign --verify --deep --strict --verbose=2 "build/Wormhole Client.app"
+spctl --assess --type execute --verbose "build/Wormhole Client.app"
+```
+
+## 📈 版本管理
+
+修改版本号:
+1. 编辑 `scripts/build-macos.sh` 中的 `VERSION` 变量
+2. 编辑 `Makefile` 中的 `VERSION` 变量
+3. 重新构建 DMG
+
+## 🤝 贡献
+
+如果您发现构建脚本的问题或有改进建议,欢迎提交 Issue 或 Pull Request。
+
+## 📄 许可证
+
+本构建系统遵循项目的 MIT 许可证。
\ No newline at end of file
diff --git a/internal/client/client.go b/internal/client/client.go
index 3d7498b..8fc0c0f 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -198,13 +198,15 @@ func (c *Client) startTransparentProxy() error {
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.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 not implemented")
+ return fmt.Errorf("transparent proxy mode is currently disabled - please use 'global' or 'http' mode")
}
// createHTTPServerWithGUI 创建带有 GUI 的 HTTP 服务器
diff --git a/internal/config/config.go b/internal/config/config.go
index 1250946..474d48a 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -21,10 +21,11 @@ type Config struct {
// Server SOCKS5服务器配置
type Server struct {
- Address string `yaml:"address"`
- Port int `yaml:"port"`
- Username string `yaml:"username"`
- Password string `yaml:"password"`
+ Address string `yaml:"address"`
+ Port int `yaml:"port"`
+ Username string `yaml:"username"`
+ Password string `yaml:"password"`
+ Timeout time.Duration `yaml:"timeout"`
}
// Proxy 代理模式配置
@@ -77,6 +78,9 @@ func LoadConfig(configPath string) (*Config, error) {
if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}
+ if config.Server.Timeout == 0 {
+ config.Server.Timeout = 60 * time.Second // 默认60秒服务器连接超时
+ }
if config.Proxy.LocalPort == 0 {
config.Proxy.LocalPort = 8080
}
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
index c84cdac..69a155e 100644
--- a/pkg/logger/logger.go
+++ b/pkg/logger/logger.go
@@ -65,20 +65,30 @@ func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
}
levelStr := ""
+ levelColor := ""
+ resetColor := "\033[0m"
+
switch level {
case DEBUG:
levelStr = "DEBUG"
+ levelColor = "\033[36m" // Cyan
case INFO:
levelStr = "INFO"
+ levelColor = "\033[32m" // Green
case WARN:
levelStr = "WARN"
+ levelColor = "\033[33m" // Yellow
case ERROR:
levelStr = "ERROR"
+ levelColor = "\033[31m" // Red
}
- timestamp := time.Now().Format("2006-01-02 15:04:05")
+ timestamp := time.Now().Format("2006-01-02T15:04:05.000Z07:00")
message := fmt.Sprintf(format, args...)
- logLine := fmt.Sprintf("[%s] [%s] %s", timestamp, levelStr, message)
+
+ // 结构化日志格式,类似logrus但保持简洁
+ logLine := fmt.Sprintf("%s[%s%s%s] [wormhole-client] %s",
+ timestamp, levelColor, levelStr, resetColor, message)
l.logger.Println(logLine)
}
diff --git a/scripts/build-dmg.sh b/scripts/build-dmg.sh
new file mode 100755
index 0000000..e66eee9
--- /dev/null
+++ b/scripts/build-dmg.sh
@@ -0,0 +1,96 @@
+#!/bin/bash
+
+set -e
+
+# 项目信息
+APP_NAME="Wormhole Client"
+VERSION="v1.0.0"
+
+# 路径配置
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+echo "🚀 Building Wormhole Client DMG Installer"
+echo "=========================================="
+echo "📦 Version: $VERSION"
+echo "📁 Project: $PROJECT_ROOT"
+echo ""
+
+# 检查必要的工具
+echo "🔍 Checking required tools..."
+
+# 检查 Go
+if ! command -v go &> /dev/null; then
+ echo "❌ Go is not installed. Please install Go first."
+ exit 1
+fi
+echo "✅ Go: $(go version)"
+
+# 检查 hdiutil (macOS 内置)
+if ! command -v hdiutil &> /dev/null; then
+ echo "❌ hdiutil not found. This script requires macOS."
+ exit 1
+fi
+echo "✅ hdiutil: Available"
+
+# 检查 osascript (macOS 内置)
+if ! command -v osascript &> /dev/null; then
+ echo "❌ osascript not found. This script requires macOS."
+ exit 1
+fi
+echo "✅ osascript: Available"
+
+echo ""
+
+# 步骤 1: 构建应用程序包
+echo "📱 Step 1: Building macOS application bundle..."
+echo "----------------------------------------------"
+if [ -f "$SCRIPT_DIR/build-macos.sh" ]; then
+ chmod +x "$SCRIPT_DIR/build-macos.sh"
+ "$SCRIPT_DIR/build-macos.sh"
+else
+ echo "❌ build-macos.sh not found!"
+ exit 1
+fi
+
+echo ""
+
+# 步骤 2: 创建 DMG
+echo "💿 Step 2: Creating DMG installer..."
+echo "------------------------------------"
+if [ -f "$SCRIPT_DIR/create-dmg.sh" ]; then
+ chmod +x "$SCRIPT_DIR/create-dmg.sh"
+ "$SCRIPT_DIR/create-dmg.sh"
+else
+ echo "❌ create-dmg.sh not found!"
+ exit 1
+fi
+
+echo ""
+
+# 完成总结
+echo "🎉 BUILD COMPLETED SUCCESSFULLY!"
+echo "================================="
+BUILD_DIR="$PROJECT_ROOT/build"
+FINAL_DMG="$BUILD_DIR/Wormhole-Client-$VERSION.dmg"
+
+if [ -f "$FINAL_DMG" ]; then
+ echo "📦 DMG File: $FINAL_DMG"
+ echo "📊 File Size: $(du -h "$FINAL_DMG" | cut -f1)"
+ echo "🔗 SHA256: $(shasum -a 256 "$FINAL_DMG" | cut -d' ' -f1)"
+ echo ""
+ echo "📋 Distribution Ready:"
+ echo "1. Test the DMG by double-clicking it"
+ echo "2. Verify the application installs and runs correctly"
+ echo "3. Share the DMG file for distribution"
+ echo ""
+ echo "🔒 Security Notes:"
+ echo "- Users may need to allow the app in System Preferences > Security & Privacy"
+ echo "- Consider code signing for easier distribution"
+ echo "- For distribution outside the App Store, users may need to right-click and 'Open'"
+ echo ""
+ echo "✨ Your macOS installer is ready!"
+else
+ echo "❌ DMG creation failed!"
+ exit 1
+fi
\ No newline at end of file
diff --git a/scripts/build-macos.sh b/scripts/build-macos.sh
new file mode 100755
index 0000000..29bae83
--- /dev/null
+++ b/scripts/build-macos.sh
@@ -0,0 +1,172 @@
+#!/bin/bash
+
+set -e
+
+# 项目信息
+APP_NAME="Wormhole Client"
+BUNDLE_ID="com.azoic.wormhole-client"
+VERSION="v1.0.0"
+BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
+
+# 路径配置
+PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+BUILD_DIR="$PROJECT_ROOT/build"
+APP_DIR="$BUILD_DIR/$APP_NAME.app"
+CONTENTS_DIR="$APP_DIR/Contents"
+MACOS_DIR="$CONTENTS_DIR/MacOS"
+RESOURCES_DIR="$CONTENTS_DIR/Resources"
+DMG_DIR="$BUILD_DIR/dmg"
+TEMP_DMG="$BUILD_DIR/temp.dmg"
+FINAL_DMG="$BUILD_DIR/Wormhole-Client-$VERSION.dmg"
+
+echo "🚀 Building Wormhole Client for macOS..."
+echo "📦 Version: $VERSION"
+echo "📅 Build Time: $BUILD_TIME"
+
+# 清理构建目录
+echo "🧹 Cleaning build directory..."
+rm -rf "$BUILD_DIR"
+mkdir -p "$BUILD_DIR"
+
+# 构建二进制文件
+echo "🔨 Building binary..."
+cd "$PROJECT_ROOT"
+GOOS=darwin GOARCH=amd64 go build \
+ -ldflags "-X main.version=$VERSION -X main.buildTime=$BUILD_TIME" \
+ -o "$BUILD_DIR/wormhole-client" \
+ cmd/wormhole-client/main.go
+
+echo "✅ Binary built successfully"
+
+# 创建应用程序包结构
+echo "📦 Creating application bundle..."
+mkdir -p "$MACOS_DIR"
+mkdir -p "$RESOURCES_DIR"
+
+# 复制二进制文件
+cp "$BUILD_DIR/wormhole-client" "$MACOS_DIR/"
+
+# 复制配置文件
+cp -r configs "$RESOURCES_DIR/"
+
+# 创建或复制应用图标
+ICON_PATH="$PROJECT_ROOT/assets/icons/app.icns"
+if [ -f "$ICON_PATH" ]; then
+ echo "📱 Using custom app icon..."
+ cp "$ICON_PATH" "$RESOURCES_DIR/"
+else
+ echo "🎨 Creating default app icon..."
+ if [ -f "$PROJECT_ROOT/scripts/create-icon.sh" ]; then
+ chmod +x "$PROJECT_ROOT/scripts/create-icon.sh"
+ "$PROJECT_ROOT/scripts/create-icon.sh"
+ if [ -f "$ICON_PATH" ]; then
+ cp "$ICON_PATH" "$RESOURCES_DIR/"
+ fi
+ fi
+fi
+
+# 创建启动脚本
+cat > "$MACOS_DIR/Wormhole Client" << 'EOF'
+#!/bin/bash
+
+# Wormhole Client macOS Launcher
+set -e
+
+# 获取应用程序路径
+APP_PATH="$(cd "$(dirname "$0")/.." && pwd)"
+RESOURCES_PATH="$APP_PATH/Resources"
+BINARY_PATH="$APP_PATH/MacOS/wormhole-client"
+CONFIG_PATH="$RESOURCES_PATH/configs/client.yaml"
+
+# 检查二进制文件
+if [ ! -f "$BINARY_PATH" ]; then
+ echo "❌ Application binary not found"
+ exit 1
+fi
+
+# 设置工作目录
+cd "$RESOURCES_PATH"
+
+# 如果没有参数,启动 HTTP 模式并打开浏览器
+if [ $# -eq 0 ]; then
+ echo "🚀 Starting Wormhole Client in HTTP proxy mode..."
+ echo "🌐 Web interface will open automatically"
+
+ # 后台启动应用
+ "$BINARY_PATH" -config "$CONFIG_PATH" -mode http &
+ APP_PID=$!
+
+ # 等待服务启动
+ sleep 3
+
+ # 打开 Web 界面
+ open "http://127.0.0.1:8080/gui" 2>/dev/null || echo "📱 Visit http://127.0.0.1:8080/gui to access the web interface"
+
+ # 等待应用程序结束
+ wait $APP_PID
+else
+ # 有参数时直接传递
+ exec "$BINARY_PATH" -config "$CONFIG_PATH" "$@"
+fi
+EOF
+
+chmod +x "$MACOS_DIR/Wormhole Client"
+
+# 创建 Info.plist
+cat > "$CONTENTS_DIR/Info.plist" << EOF
+
+
+