diff --git a/.gitignore b/.gitignore
index e1d2776..5cca6c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,7 +37,7 @@ go.work
.Trashes
ehthumbs.db
Thumbs.db
-
+.github/
# Logs
*.log
logs/
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..5beb931
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,55 @@
+run:
+ timeout: 5m
+ tests: true
+
+linters:
+ enable:
+ - errcheck # 检查未处理的错误
+ - gosimple # 建议代码简化
+ - govet # Go静态分析
+ - ineffassign # 检查无效赋值
+ - staticcheck # 静态分析
+ - typecheck # 类型检查
+ - unused # 检查未使用的代码
+ - gofmt # 检查代码格式
+ - goimports # 检查导入顺序
+ - misspell # 检查拼写错误
+ - unconvert # 检查不必要的类型转换
+ - unparam # 检查未使用的参数
+ - gosec # 安全检查
+ - gocritic # Go代码审查
+
+linters-settings:
+ gosec:
+ excludes:
+ - G204 # 子进程审计(我们需要执行系统命令)
+
+ gocritic:
+ enabled-tags:
+ - diagnostic
+ - style
+ - performance
+ disabled-checks:
+ - paramTypeCombine
+ - whyNoLint
+
+issues:
+ exclude-rules:
+ # 忽略测试文件中的错误检查
+ - path: _test\.go
+ linters:
+ - errcheck
+ - gosec
+
+ # 忽略生成的文件
+ - path: ".*\\.pb\\.go"
+ linters:
+ - all
+
+ max-issues-per-linter: 0
+ max-same-issues: 0
+
+output:
+ format: colored-line-number
+ print-issued-lines: true
+ print-linter-name: true
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..64230ed
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Wormhole SOCKS5 Client
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 5ab751d..b7704ba 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
+.PHONY: all build clean deps test run install uninstall lint coverage fmt vet check
all: clean deps build
@@ -14,6 +14,28 @@ deps:
build:
$(GO) build $(LDFLAGS) -o bin/$(APP_NAME) cmd/wormhole-client/main.go
+# 格式化代码
+fmt:
+ $(GO) fmt ./...
+
+# 静态分析
+vet:
+ $(GO) vet ./...
+
+# 代码检查 (需要安装 golangci-lint)
+lint:
+ @which golangci-lint > /dev/null || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest)
+ golangci-lint run
+
+# 运行测试并生成覆盖率报告
+coverage:
+ $(GO) test -v -race -coverprofile=coverage.out ./...
+ $(GO) tool cover -html=coverage.out -o coverage.html
+ @echo "Coverage report generated: coverage.html"
+
+# 完整的代码检查
+check: fmt vet lint test
+
run-http: build
./bin/$(APP_NAME) -config configs/client.yaml -mode http
@@ -27,7 +49,7 @@ test:
$(GO) test -v ./...
clean:
- rm -rf bin/
+ rm -rf bin/ coverage.out coverage.html
install: build
sudo cp bin/$(APP_NAME) /usr/local/bin/
@@ -38,10 +60,15 @@ uninstall:
help:
@echo "Available targets:"
@echo " build - Build the client binary"
+ @echo " fmt - Format Go code"
+ @echo " vet - Run Go vet"
+ @echo " lint - Run golangci-lint"
+ @echo " test - Run tests"
+ @echo " coverage - Run tests with coverage report"
+ @echo " check - Run all code quality checks"
@echo " run-http - Run HTTP proxy mode"
@echo " run-global - Run global proxy mode (needs sudo)"
@echo " run-transparent - Run transparent proxy mode (needs sudo)"
- @echo " test - Run tests"
@echo " clean - Clean build artifacts"
@echo " deps - Download dependencies"
@echo " install - Install to /usr/local/bin"
diff --git a/README.md b/README.md
index a0dc48d..f42d838 100644
--- a/README.md
+++ b/README.md
@@ -1,100 +1,234 @@
# Wormhole SOCKS5 Client
-[](https://go.dev/)
-[](LICENSE)
-[]()
+[](https://golang.org)
+[](LICENSE)
+[](.github/workflows)
-一个功能强大的 SOCKS5 代理客户端,支持多种代理模式,包括 HTTP 代理转换、全局代理设置和透明代理。
+一个功能强大的 SOCKS5 代理客户端,支持 HTTP 代理转换、全局代理、智能分流和实时监控。
-## ✨ 功能特性
+## ✨ **核心特性**
-- 🚀 **HTTP 代理模式**: 将 SOCKS5 代理转换为 HTTP/HTTPS 代理
-- 🌍 **全局代理模式**: 自动配置系统级代理设置
-- 🔍 **DNS 代理**: 提供 DNS 请求代理和缓存功能
-- 🖥️ **系统集成**: 支持 macOS 系统代理自动配置和恢复
-- 📝 **灵活配置**: 支持 YAML 配置文件和命令行参数
-- 🛡️ **安全认证**: 支持 SOCKS5 用户名密码认证
-- ⚡ **高性能**: 异步处理和连接池优化
+### 🌐 **多种代理模式**
+- ✅ **HTTP 代理模式**: 将 SOCKS5 转换为 HTTP/HTTPS 代理,兼容浏览器和应用
+- ✅ **全局代理模式**: 自动配置系统级代理,支持 macOS/Windows/Linux
+- ⚠️ **透明代理模式**: 透明流量拦截(计划实现)
-## 🚀 快速开始
+### 🛣️ **智能路由分流**
+- 🎯 **精确匹配**: 支持域名通配符和正则表达式
+- 🏠 **本地绕过**: 自动识别本地网络和私有网络
+- 🌍 **国际访问**: 特定域名强制走代理(Google、GitHub、Twitter等)
+- 🇨🇳 **国内直连**: 中国域名和服务直接访问
-### 安装
+### 📊 **实时监控**
+- 📈 **详细统计**: 连接数、成功率、流量统计、错误分类
+- 💚 **健康检查**: RESTful API 端点,支持监控系统集成
+- 🔍 **性能指标**: 响应时间、并发连接、字节传输统计
-#### 从源码编译
+### 🔒 **安全特性**
+- 🛡️ **认证支持**: 用户名密码认证和无认证模式
+- 🌐 **协议完整**: 完整的 SOCKS5 协议实现,支持 IPv4/IPv6
+- 🔐 **DNS 安全**: 防止 DNS 污染和泄露
+
+## 🚀 **快速开始**
+
+### 安装方式
```bash
+# 方式 1: 从源码编译
git clone http://101.34.16.52:3000/huyinsong/wormhole-client.git
cd wormhole-client
make build
+
+# 方式 2: 直接下载二进制文件
+# (根据你的系统下载对应版本)
```
-#### 使用预编译二进制
+### 基本使用
+
+#### 1. HTTP 代理模式 (推荐新手)
```bash
-# 安装到系统路径
-make install
+# 启动 HTTP 代理
+./bin/wormhole-client -mode http
+
+# 配置浏览器代理: 127.0.0.1:8080
+# 访问: http://127.0.0.1:8080/stats (查看统计)
```
-### 基本使用
+#### 2. 全局代理模式 (推荐进阶用户)
-1. **配置 SOCKS5 服务器信息**
+```bash
+# macOS/Linux (需要 sudo)
+sudo ./bin/wormhole-client -mode global
+
+# Windows (以管理员身份运行)
+./bin/wormhole-client.exe -mode global
+```
-编辑 `configs/client.yaml`:
+**全局代理功能**:
+- 🔧 **自动配置**: 自动设置系统 HTTP/HTTPS 代理
+- 🌍 **全局生效**: 所有应用程序自动使用代理
+- 🛣️ **智能分流**: 根据域名规则自动选择直连或代理
+- 🔍 **DNS 代理**: 防止 DNS 污染,支持自定义 DNS 服务器
+- 💻 **跨平台**: 支持 macOS、Windows、Linux 系统
+
+#### 3. 配置文件示例
```yaml
+# configs/client.yaml
server:
- address: your-socks5-server.com
+ address: your_socks5_server.com
port: 1080
- username: your-username
- password: your-password
+ username: your_username
+ password: your_password
+
+proxy:
+ mode: global
+ localPort: 8080
+
+globalProxy:
+ enabled: true
+ dnsProxy: true
+ dnsPort: 5353
+
+ routing:
+ bypassLocal: true
+ bypassPrivate: true
+ bypassDomains:
+ - "*.cn"
+ - "*.baidu.com"
+ - "*.qq.com"
+ forceDomains:
+ - "*.google.com"
+ - "*.github.com"
+ - "*.youtube.com"
```
-2. **启动 HTTP 代理模式**
+## 📋 **详细功能**
+
+### HTTP 代理模式
+
+将 SOCKS5 代理转换为标准的 HTTP 代理:
```bash
./bin/wormhole-client -mode http
```
-3. **配置浏览器代理**
- - HTTP 代理: `127.0.0.1:8080`
- - HTTPS 代理: `127.0.0.1:8080`
+- 📱 **浏览器兼容**: 支持所有主流浏览器
+- 🔗 **HTTPS 支持**: 完整的 CONNECT 方法支持
+- 📊 **实时监控**: 内置 Web 管理界面
-## 📖 使用模式
+**使用场景**:
+- 浏览器科学上网
+- 开发调试代理
+- 应用程序代理配置
-### HTTP 代理模式
+### 全局代理模式
-最常用的模式,将 SOCKS5 代理转换为 HTTP 代理:
+自动配置系统级代理设置:
```bash
-# 使用默认配置
-./bin/wormhole-client -mode http
+# 启动全局代理 (需要管理员权限)
+sudo ./bin/wormhole-client -mode global
+```
+
+**核心优势**:
+- 🌍 **系统级代理**: 自动配置系统代理设置
+- 🚀 **零配置**: 启动即用,无需手动设置
+- 🛣️ **智能分流**: 国内外流量自动分流
+- 🔍 **DNS 代理**: 解决 DNS 污染问题
+- 🔄 **优雅退出**: 程序退出时自动恢复原始设置
+
+**系统支持**:
+- 🍎 **macOS**: 通过 `networksetup` 配置网络偏好设置
+- 🪟 **Windows**: 通过注册表配置 Internet 设置
+- 🐧 **Linux**: 通过环境变量和系统文件配置
-# 指定端口
-./bin/wormhole-client -mode http -config configs/custom.yaml
+### 智能路由分流
+
+高级流量分流功能:
+
+```yaml
+routing:
+ # 直连规则
+ bypassLocal: true # 本地地址直连
+ bypassPrivate: true # 私有网络直连
+ bypassDomains: # 指定域名直连
+ - "*.cn" # 中国域名
+ - "*.baidu.com" # 百度服务
+
+ # 代理规则
+ forceDomains: # 强制代理域名
+ - "*.google.com" # Google 服务
+ - "*.github.com" # GitHub
+ - "*.youtube.com" # YouTube
```
-### 全局代理模式
+**智能判断逻辑**:
+1. 🎯 **强制代理**: 匹配 `forceDomains` 的域名强制走代理
+2. 🏠 **直连绕过**: 匹配 `bypassDomains` 或本地网络直接访问
+3. 🤖 **自动决策**: 其他流量根据配置自动选择
-自动配置系统代理,适合需要全局科学上网的场景:
+## 🔧 **命令行选项**
```bash
-# macOS 需要管理员权限
-sudo ./bin/wormhole-client -mode global
+./bin/wormhole-client [选项]
+
+选项:
+ -config string
+ 配置文件路径 (默认 "configs/client.yaml")
+ -mode string
+ 代理模式: http, global, transparent (默认 "http")
+ -version
+ 显示版本信息
+ -help
+ 显示帮助信息
```
-**特性:**
-- 自动配置系统 HTTP/HTTPS 代理
-- 可选的 DNS 代理功能
-- 程序退出时自动恢复原始设置
-- 支持 Ctrl+C 安全退出
+## 📊 **监控和管理**
+
+### Web 管理界面
-### 透明代理模式 (开发中)
+启动后访问以下端点:
+
+- 📊 **统计信息**: `http://127.0.0.1:8080/stats`
+- 💚 **健康检查**: `http://127.0.0.1:8080/health`
+- 🌐 **代理服务**: `http://127.0.0.1:8080/` (HTTP 代理入口)
+
+### API 示例
```bash
-sudo ./bin/wormhole-client -mode transparent
+# 获取统计信息
+curl http://127.0.0.1:8080/stats
+
+# 健康检查
+curl http://127.0.0.1:8080/health
+
+# 实时监控
+watch -n 5 'curl -s http://127.0.0.1:8080/health | jq .success_rate'
+```
+
+### 统计信息
+
+```json
+{
+ "start_time": "2024-01-01T12:00:00Z",
+ "uptime": "2h30m15s",
+ "total_connections": 150,
+ "active_connections": 5,
+ "successful_requests": 145,
+ "failed_requests": 5,
+ "success_rate": 96.67,
+ "bytes_sent": 1024000,
+ "bytes_received": 2048000,
+ "socks5_errors": {
+ "connection_failed": 3,
+ "auth_failed": 1
+ }
+}
```
-## ⚙️ 配置说明
+## ⚙️ **配置说明**
### 完整配置示例
@@ -108,46 +242,49 @@ server:
# 代理模式设置
proxy:
- mode: http
+ mode: global
localPort: 8080
# 全局代理设置
globalProxy:
- enabled: false
+ enabled: true
dnsProxy: true
dnsPort: 5353
- # 路由规则
+ # 智能分流路由规则
routing:
- bypassLocal: true # 跳过本地地址
- bypassPrivate: true # 跳过私有网络
- bypassDomains: # 跳过的域名
+ bypassLocal: true
+ bypassPrivate: true
+ bypassDomains:
- "*.local"
- - "*.lan"
- forceDomains: # 强制代理的域名
+ - "*.cn"
+ - "*.baidu.com"
+ forceDomains:
- "*.google.com"
- "*.github.com"
+# 性能调优
+performance:
+ connectionPool:
+ maxSize: 100
+ maxIdleTime: "300s"
+ timeouts:
+ connect: "10s"
+ read: "30s"
+
+# 安全设置
+security:
+ verifySSL: true
+ allowedProtocols:
+ - "http"
+ - "https"
+
# 日志设置
-logLevel: info # debug, info, warn, error
+logLevel: info
timeout: 30s
```
-### 命令行选项
-
-```bash
-./bin/wormhole-client [选项]
-
-选项:
- -config string
- 配置文件路径 (默认 "configs/client.yaml")
- -mode string
- 代理模式: http, global, transparent (默认 "http")
- -version
- 显示版本信息
-```
-
-## 🏗️ 项目结构
+## 🏗️ **项目结构**
```
wormhole-client/
@@ -156,21 +293,28 @@ wormhole-client/
│ ├── client/ # 客户端核心逻辑
│ ├── config/ # 配置解析模块
│ ├── proxy/ # SOCKS5 代理实现
+│ ├── routing/ # 智能路由模块
│ └── system/ # 系统代理管理
├── pkg/ # 公共模块
│ ├── dns/ # DNS 代理模块
│ └── logger/ # 日志模块
├── configs/ # 配置文件
├── docs/ # 文档
+│ ├── api.md # API 文档
+│ ├── global-proxy.md # 全局代理使用指南
+│ └── usage.md # 使用说明
├── scripts/ # 构建脚本
└── bin/ # 编译输出
```
-## 🔧 开发
+## 🔧 **开发**
### 编译和测试
```bash
+# 安装依赖
+make deps
+
# 编译
make build
@@ -180,73 +324,164 @@ make test
# 代码检查
make lint
-# 清理
-make clean
+# 生成覆盖率报告
+make coverage
-# 开发模式运行
-make dev
+# 完整检查
+make check
```
-### 添加新功能
+### 运行模式
-1. Fork 本项目
-2. 创建功能分支
-3. 编写代码和测试
-4. 提交 Pull Request
+```bash
+# HTTP 代理模式
+make run-http
+
+# 全局代理模式 (需要 sudo)
+make run-global
+
+# 透明代理模式 (开发中)
+make run-transparent
+```
+
+## 📖 **使用场景**
+
+### 开发者场景
+
+```yaml
+# 适合开发者的全局代理配置
+globalProxy:
+ routing:
+ forceDomains:
+ - "*.github.com"
+ - "*.stackoverflow.com"
+ - "*.npmjs.com"
+ - "*.docker.com"
+ bypassDomains:
+ - "*.local"
+ - "localhost"
+```
+
+### 办公环境
+
+```yaml
+# 适合办公环境的配置
+globalProxy:
+ routing:
+ bypassLocal: true
+ bypassPrivate: true
+ forceDomains:
+ - "*.google.com"
+ - "*.zoom.us"
+ - "*.dropbox.com"
+```
-## 📚 文档
+### 家庭使用
-- [使用指南](docs/usage.md) - 详细的使用说明
-- [配置参考](docs/configuration.md) - 配置参数说明
-- [故障排除](docs/troubleshooting.md) - 常见问题解决
+```yaml
+# 适合家庭用户的配置
+globalProxy:
+ routing:
+ forceDomains:
+ - "*.youtube.com"
+ - "*.netflix.com"
+ - "*.twitter.com"
+ bypassDomains:
+ - "*.cn"
+ - "*.baidu.com"
+```
-## 🐛 故障排除
+## 🔧 **故障排除**
### 常见问题
-1. **连接失败**
- - 检查 SOCKS5 服务器地址和端口
- - 验证用户名和密码
- - 确认网络连通性
+#### 1. 权限问题
-2. **权限问题** (macOS)
- ```bash
- sudo ./bin/wormhole-client -mode global
- ```
+```bash
+❌ Failed to set system proxy: permission denied
+```
-3. **端口占用**
- - 修改配置文件中的 `localPort`
- - 检查端口是否被其他程序占用
+**解决方案**:
+- macOS/Linux: 使用 `sudo ./bin/wormhole-client -mode global`
+- Windows: 以管理员身份运行程序
-### 调试模式
+#### 2. 连接失败
```bash
+❌ Failed to connect via SOCKS5: connection refused
+```
+
+**解决方案**:
+- 检查 SOCKS5 服务器地址和端口
+- 验证用户名和密码
+- 确认网络连通性
+
+#### 3. 端口占用
+
+```bash
+⚠️ Failed to start DNS proxy: bind: address already in use
+```
+
+**解决方案**:
+- 更改端口: `localPort: 8081`
+- 停止占用端口的其他程序
+
+### 调试模式
+
+```yaml
# 启用详细日志
-./bin/wormhole-client -mode http
+logLevel: debug
+```
+
+或使用环境变量:
+
+```bash
+export LOG_LEVEL=debug
+./bin/wormhole-client -mode global
```
-在配置文件中设置 `logLevel: debug` 获取详细信息。
+## 🛡️ **安全注意事项**
+
+1. **配置文件安全**:
+ - 使用强密码
+ - 设置文件权限: `chmod 600 configs/client.yaml`
+ - 避免在公开仓库提交密码
-## 🛡️ 安全说明
+2. **系统安全**:
+ - 仅在需要时使用管理员权限
+ - 定期更新客户端版本
+ - 使用可信的 SOCKS5 服务器
-- 保护好包含密码的配置文件
-- 仅在需要时使用管理员权限
-- 确保 SOCKS5 服务器的安全性
-- 使用 DNS 代理防止 DNS 泄露
+3. **网络安全**:
+ - 启用 SSL 证书验证
+ - 定期检查代理设置
+ - 监控异常流量
-## 📄 许可证
+## 📚 **文档链接**
-本项目采用 [MIT 许可证](LICENSE)。
+- 📖 [全局代理使用指南](docs/global-proxy.md) - 详细的全局代理配置和使用说明
+- 📊 [API 文档](docs/api.md) - RESTful API 接口说明
+- 🔧 [配置参考](configs/client.yaml) - 完整的配置文件示例
+- 📋 [使用说明](docs/usage.md) - 各种使用场景和技巧
-## 🤝 贡献
+## 🤝 **贡献**
欢迎提交 Issue 和 Pull Request!
-## 📞 联系
+1. Fork 项目
+2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
+3. 提交更改 (`git commit -m 'Add amazing feature'`)
+4. 推送分支 (`git push origin feature/amazing-feature`)
+5. 创建 Pull Request
+
+## 📄 **许可证**
+
+本项目采用 [MIT License](LICENSE) 许可证。
+
+## 🙏 **致谢**
-- 项目地址: http://101.34.16.52:3000/huyinsong/wormhole-client.git
-- 问题反馈: 请使用 GitHub Issues
+感谢所有贡献者和使用者的支持!
---
-**⭐ 如果这个项目对你有帮助,请给一个 Star!**
+**⭐ 如果这个项目对你有帮助,请给个 Star!**
diff --git a/configs/client.yaml b/configs/client.yaml
index f9527dd..226cb15 100644
--- a/configs/client.yaml
+++ b/configs/client.yaml
@@ -10,35 +10,195 @@ server:
# 代理模式设置
proxy:
- mode: http # http, global, transparent
+ mode: global # http, global, transparent
localPort: 8080
# 全局代理设置
globalProxy:
- enabled: false
+ enabled: true
dnsProxy: true
dnsPort: 5353
- # 分流规则
+ # 智能分流路由规则
routing:
- bypassLocal: true
- bypassPrivate: true
+ # 基础规则
+ bypassLocal: true # 绕过本地地址 (localhost, 127.0.0.1等)
+ bypassPrivate: true # 绕过私有网络 (192.168.x.x, 10.x.x.x等)
+
+ # 绕过域名列表 (直连,不经过代理)
bypassDomains:
+ # 本地域名
- "*.local"
+ - "*.localhost"
- "*.lan"
+ - "*.internal"
+ - "*.corp"
+
+ # 国内常用域名
+ - "*.cn"
+ - "*.baidu.com"
+ - "*.qq.com"
+ - "*.weixin.qq.com"
+ - "*.taobao.com"
+ - "*.tmall.com"
+ - "*.alipay.com"
+ - "*.jd.com"
+ - "*.163.com"
+ - "*.sina.com.cn"
+ - "*.sohu.com"
+ - "*.youku.com"
+ - "*.bilibili.com"
+ - "*.douyin.com"
+ - "*.tiktok.com"
+
+ # CDN和云服务
+ - "*.cloudflare.com"
+ - "*.amazonaws.com"
+ - "*.aliyuncs.com"
+ - "*.qcloud.com"
+
+ # 强制代理域名列表 (必须经过代理)
forceDomains:
+ # Google 服务
- "*.google.com"
+ - "*.googlepai.com"
+ - "*.googleapis.com"
+ - "*.googleusercontent.com"
+ - "*.googlevideo.com"
+ - "*.gstatic.com"
+ - "*.gmail.com"
+ - "*.youtube.com"
+ - "*.youtu.be"
+ - "*.ytimg.com"
+
+ # 社交媒体
+ - "*.facebook.com"
+ - "*.fbcdn.net"
+ - "*.instagram.com"
+ - "*.twitter.com"
+ - "*.twimg.com"
+ - "*.t.co"
+ - "*.linkedin.com"
+ - "*.pinterest.com"
+ - "*.reddit.com"
+ - "*.snapchat.com"
+ - "*.discord.com"
+ - "*.telegram.org"
+ - "*.whatsapp.com"
+
+ # 技术开发
- "*.github.com"
+ - "*.githubusercontent.com"
+ - "*.github.io"
+ - "*.stackoverflow.com"
+ - "*.stackexchange.com"
+ - "*.medium.com"
+ - "*.dev.to"
+ - "*.npmjs.com"
+ - "*.pypi.org"
+ - "*.docker.com"
+ - "*.hub.docker.com"
+
+ # 新闻媒体
+ - "*.nytimes.com"
+ - "*.washingtonpost.com"
+ - "*.wsj.com"
+ - "*.reuters.com"
+ - "*.bbc.com"
+ - "*.cnn.com"
+ - "*.bloomberg.com"
+
+ # 其他服务
+ - "*.dropbox.com"
+ - "*.onedrive.com"
+ - "*.zoom.us"
+ - "*.spotify.com"
+ - "*.netflix.com"
+ - "*.hulu.com"
+ - "*.twitch.tv"
+ - "*.steam.community"
-# 透明代理设置
+# 透明代理设置 (实验性功能)
transparentProxy:
enabled: false
port: 8080
dnsPort: 5353
- # 系统设置
- modifyDNS: true
- modifyRoute: true
+ # 系统级修改 (需要root权限)
+ modifyDNS: true # 修改系统DNS设置
+ modifyRoute: true # 修改路由表
+
+ # iptables规则 (Linux only)
+ iptables:
+ enabled: false
+ table: "nat"
+ chain: "OUTPUT"
+
+# 性能调优
+performance:
+ # 连接池设置
+ connectionPool:
+ maxSize: 100
+ maxIdleTime: "300s"
+
+ # 超时设置
+ timeouts:
+ connect: "10s"
+ handshake: "5s"
+ read: "30s"
+ write: "30s"
+
+ # 缓存设置
+ cache:
+ dnsCache: true
+ dnsCacheSize: 1000
+ dnsCacheTTL: "3600s"
+
+# 安全设置
+security:
+ # 证书验证
+ verifySSL: true
+ # 允许的协议
+ allowedProtocols:
+ - "http"
+ - "https"
+ - "socks5"
+
+ # 黑名单
+ blacklist:
+ domains: []
+ ips: []
+
+# 监控和统计
+monitoring:
+ # 统计信息
+ stats:
+ enabled: true
+ interval: "60s"
+
+ # 健康检查
+ healthCheck:
+ enabled: true
+ interval: "30s"
+
+ # 日志设置
+ logging:
+ level: info # debug, info, warn, error
+ file: "" # 留空表示输出到控制台
+ maxSize: "100MB" # 日志文件最大大小
+ maxBackups: 5 # 保留的备份文件数
+ maxAge: "30d" # 日志文件保留天数
+ compress: true # 是否压缩旧日志
+
+# 全局设置
logLevel: info
timeout: 30s
+
+# Web管理界面 (可选)
+webUI:
+ enabled: false
+ port: 8081
+ username: "admin"
+ password: "wormhole123"
+ theme: "dark"
diff --git a/coverage.html b/coverage.html
new file mode 100644
index 0000000..c203ced
--- /dev/null
+++ b/coverage.html
@@ -0,0 +1,1693 @@
+
+
+
+
+
+
+
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/api.md b/docs/api.md
new file mode 100644
index 0000000..637bd6f
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,231 @@
+# Wormhole SOCKS5 Client API
+
+## 概述
+
+Wormhole SOCKS5 Client 在HTTP代理模式下提供了内置的API端点,用于监控和管理代理服务。
+
+## 端点
+
+### 代理功能
+
+所有非API请求都会通过SOCKS5代理转发:
+
+- **HTTP 代理**: 处理普通HTTP请求
+- **HTTPS 代理**: 处理CONNECT方法的HTTPS隧道
+
+### 监控API
+
+#### GET /stats
+
+获取详细的代理统计信息。
+
+**响应示例:**
+```json
+{
+ "start_time": "2024-01-01T12:00:00Z",
+ "uptime": "2h30m15s",
+ "total_connections": 150,
+ "active_connections": 5,
+ "successful_requests": 145,
+ "failed_requests": 5,
+ "bytes_sent": 1024000,
+ "bytes_received": 2048000,
+ "socks5_errors": {
+ "connection_failed": 3,
+ "auth_failed": 1,
+ "timeout": 1
+ }
+}
+```
+
+**字段说明:**
+- `start_time`: 代理启动时间
+- `uptime`: 运行时间
+- `total_connections`: 总连接数
+- `active_connections`: 当前活跃连接数
+- `successful_requests`: 成功请求数
+- `failed_requests`: 失败请求数
+- `bytes_sent`: 发送字节数
+- `bytes_received`: 接收字节数
+- `socks5_errors`: SOCKS5错误分类统计
+
+#### GET /health
+
+获取代理健康状态信息。
+
+**响应示例:**
+```json
+{
+ "status": "healthy",
+ "uptime": "2h30m15s",
+ "active_connections": 5,
+ "success_rate": 96.67
+}
+```
+
+**字段说明:**
+- `status`: 健康状态 ("healthy")
+- `uptime`: 运行时间
+- `active_connections`: 当前活跃连接数
+- `success_rate`: 成功率百分比
+
+## 使用示例
+
+### 基本用法
+
+1. **启动HTTP代理模式:**
+ ```bash
+ ./bin/wormhole-client -mode http -config configs/client.yaml
+ ```
+
+2. **配置浏览器代理:**
+ - HTTP代理: `127.0.0.1:8080`
+ - HTTPS代理: `127.0.0.1:8080`
+
+3. **访问统计信息:**
+ ```bash
+ curl http://127.0.0.1:8080/stats
+ ```
+
+4. **检查健康状态:**
+ ```bash
+ curl http://127.0.0.1:8080/health
+ ```
+
+### 监控脚本示例
+
+#### Bash监控脚本
+
+```bash
+#!/bin/bash
+
+PROXY_URL="http://127.0.0.1:8080"
+
+echo "=== Wormhole SOCKS5 Client Status ==="
+
+# 健康检查
+health=$(curl -s "$PROXY_URL/health")
+echo "Health: $health"
+
+# 统计信息
+stats=$(curl -s "$PROXY_URL/stats")
+echo "Stats: $stats"
+
+# 提取关键指标
+success_rate=$(echo "$health" | jq -r '.success_rate')
+active_conn=$(echo "$health" | jq -r '.active_connections')
+
+echo "Success Rate: ${success_rate}%"
+echo "Active Connections: $active_conn"
+```
+
+#### Python监控脚本
+
+```python
+import requests
+import json
+import time
+
+def get_proxy_stats():
+ try:
+ response = requests.get('http://127.0.0.1:8080/stats')
+ return response.json()
+ except Exception as e:
+ print(f"Error getting stats: {e}")
+ return None
+
+def get_proxy_health():
+ try:
+ response = requests.get('http://127.0.0.1:8080/health')
+ return response.json()
+ except Exception as e:
+ print(f"Error getting health: {e}")
+ return None
+
+def monitor_proxy():
+ while True:
+ health = get_proxy_health()
+ if health:
+ print(f"Status: {health['status']}")
+ print(f"Success Rate: {health['success_rate']:.2f}%")
+ print(f"Active Connections: {health['active_connections']}")
+ print(f"Uptime: {health['uptime']}")
+
+ print("-" * 40)
+ time.sleep(30) # 每30秒检查一次
+
+if __name__ == "__main__":
+ monitor_proxy()
+```
+
+## SOCKS5协议支持
+
+### 地址类型支持
+
+- ✅ **IPv4地址** (0x01): `192.168.1.1:80`
+- ✅ **域名** (0x03): `example.com:80`
+- ✅ **IPv6地址** (0x04): `[2001:db8::1]:80`
+
+### 认证方法支持
+
+- ✅ **无认证** (0x00)
+- ✅ **用户名密码认证** (0x02)
+
+### 错误处理
+
+代理能够处理并报告以下SOCKS5错误:
+
+| 错误码 | 含义 | 统计字段 |
+|--------|------|----------|
+| 0x01 | 一般SOCKS服务器故障 | `general_failure` |
+| 0x02 | 连接不被规则集允许 | `not_allowed` |
+| 0x03 | 网络不可达 | `network_unreachable` |
+| 0x04 | 主机不可达 | `host_unreachable` |
+| 0x05 | 连接被拒绝 | `connection_refused` |
+| 0x06 | TTL过期 | `ttl_expired` |
+| 0x07 | 不支持的命令 | `command_not_supported` |
+| 0x08 | 不支持的地址类型 | `address_not_supported` |
+
+## 性能特性
+
+### 连接管理
+- 异步处理多个并发连接
+- 自动资源清理
+- 超时管理
+
+### 统计精度
+- 原子操作保证并发安全
+- 实时字节传输统计
+- 详细的错误分类
+
+### HTTP服务器配置
+- 读取超时: 30秒
+- 写入超时: 30秒
+- 空闲超时: 120秒
+- 最大头部大小: 1MB
+
+## 故障排除
+
+### 常见问题
+
+1. **无法访问统计API**
+ - 确认代理运行在HTTP模式
+ - 检查端口是否正确
+ - 确认防火墙设置
+
+2. **统计数据不准确**
+ - 重启代理服务
+ - 检查并发连接情况
+
+3. **高错误率**
+ - 检查SOCKS5服务器状态
+ - 验证认证信息
+ - 查看网络连接质量
+
+### 日志级别
+
+设置 `logLevel: debug` 可以获得详细的调试信息:
+
+```yaml
+logLevel: debug # debug, info, warn, error
+```
\ No newline at end of file
diff --git a/docs/global-proxy.md b/docs/global-proxy.md
new file mode 100644
index 0000000..6d03540
--- /dev/null
+++ b/docs/global-proxy.md
@@ -0,0 +1,379 @@
+# 全局代理模式使用指南
+
+## 概述
+
+全局代理模式是 Wormhole SOCKS5 Client 的核心功能之一,它可以自动配置系统级代理设置,让所有应用程序都通过 SOCKS5 代理访问网络。
+
+## 🌟 **主要特性**
+
+### ✅ **系统级代理配置**
+- **macOS**: 自动配置网络偏好设置中的 HTTP/HTTPS 代理
+- **Windows**: 通过注册表配置系统代理设置
+- **Linux**: 配置环境变量和系统代理文件
+
+### ✅ **智能路由分流**
+- **绕过规则**: 本地网络、私有网络、指定域名直连
+- **强制代理**: 特定域名强制走代理(如 Google、GitHub 等)
+- **自动决策**: 其他流量根据配置自动选择路由
+
+### ✅ **DNS 代理支持**
+- 防止 DNS 污染和泄露
+- 支持自定义 DNS 服务器
+- 提供 DNS 缓存功能
+
+### ✅ **实时监控**
+- Web 界面统计信息
+- 健康状态检查
+- 详细的连接和字节传输统计
+
+## 🚀 **快速开始**
+
+### 1. 基本配置
+
+编辑配置文件 `configs/client.yaml`:
+
+```yaml
+# SOCKS5 服务器设置
+server:
+ address: your_socks5_server.com # 替换为你的 SOCKS5 服务器
+ port: 1080
+ username: your_username # 替换为用户名
+ password: your_password # 替换为密码
+
+# 启用全局代理模式
+proxy:
+ mode: global
+ localPort: 8080
+
+# 全局代理配置
+globalProxy:
+ enabled: true
+ dnsProxy: true
+ dnsPort: 5353
+```
+
+### 2. 启动全局代理
+
+```bash
+# macOS/Linux (需要管理员权限)
+sudo ./bin/wormhole-client -mode global
+
+# Windows (以管理员身份运行)
+./bin/wormhole-client.exe -mode global
+```
+
+### 3. 验证配置
+
+启动后检查以下信息:
+
+```
+🌍 Starting global proxy mode...
+✅ System proxy configured successfully
+✅ DNS proxy started
+🛣️ Route matcher initialized
+📊 Statistics: http://127.0.0.1:8080/stats
+💚 Health check: http://127.0.0.1:8080/health
+```
+
+## ⚙️ **高级配置**
+
+### 智能分流规则
+
+```yaml
+globalProxy:
+ routing:
+ # 基础规则
+ bypassLocal: true # 绕过本地地址
+ bypassPrivate: true # 绕过私有网络
+
+ # 直连域名 (不走代理)
+ bypassDomains:
+ - "*.cn" # 中国域名
+ - "*.baidu.com" # 百度系列
+ - "*.taobao.com" # 淘宝系列
+ - "*.qq.com" # 腾讯系列
+ - "*.local" # 本地域名
+
+ # 强制代理域名 (必须走代理)
+ forceDomains:
+ - "*.google.com" # Google 服务
+ - "*.youtube.com" # YouTube
+ - "*.github.com" # GitHub
+ - "*.twitter.com" # Twitter
+ - "*.facebook.com" # Facebook
+```
+
+### 性能优化
+
+```yaml
+# 性能调优配置
+performance:
+ connectionPool:
+ maxSize: 100 # 连接池大小
+ maxIdleTime: "300s" # 最大空闲时间
+
+ timeouts:
+ connect: "10s" # 连接超时
+ handshake: "5s" # 握手超时
+ read: "30s" # 读取超时
+ write: "30s" # 写入超时
+
+ cache:
+ dnsCache: true # 启用 DNS 缓存
+ dnsCacheSize: 1000 # 缓存大小
+ dnsCacheTTL: "3600s" # 缓存生存时间
+```
+
+## 🖥️ **系统特定配置**
+
+### macOS 配置
+
+macOS 系统会自动配置以下设置:
+
+```bash
+# 查看当前代理配置
+networksetup -getwebproxy Wi-Fi
+networksetup -getsecurewebproxy Wi-Fi
+
+# 手动配置 (如果自动配置失败)
+networksetup -setwebproxy Wi-Fi 127.0.0.1 8080
+networksetup -setsecurewebproxy Wi-Fi 127.0.0.1 8080
+```
+
+### Windows 配置
+
+Windows 系统通过注册表配置:
+
+```powershell
+# 查看当前代理设置
+$regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
+Get-ItemProperty -Path $regPath -Name ProxyEnable
+Get-ItemProperty -Path $regPath -Name ProxyServer
+
+# 手动配置 (如果自动配置失败)
+Set-ItemProperty -Path $regPath -Name ProxyEnable -Value 1
+Set-ItemProperty -Path $regPath -Name ProxyServer -Value "127.0.0.1:8080"
+```
+
+### Linux 配置
+
+Linux 系统通过环境变量配置:
+
+```bash
+# 查看当前代理设置
+echo $http_proxy
+echo $https_proxy
+
+# 手动配置 (如果自动配置失败)
+export http_proxy=http://127.0.0.1:8080
+export https_proxy=http://127.0.0.1:8080
+export HTTP_PROXY=http://127.0.0.1:8080
+export HTTPS_PROXY=http://127.0.0.1:8080
+```
+
+## 📊 **监控和管理**
+
+### Web 管理界面
+
+访问以下地址查看统计信息:
+
+- **统计信息**: http://127.0.0.1:8080/stats
+- **健康检查**: http://127.0.0.1:8080/health
+
+### 统计信息示例
+
+```json
+{
+ "start_time": "2024-01-01T12:00:00Z",
+ "uptime": "2h30m15s",
+ "total_connections": 150,
+ "active_connections": 5,
+ "successful_requests": 145,
+ "failed_requests": 5,
+ "success_rate": 96.67,
+ "bytes_sent": 1024000,
+ "bytes_received": 2048000,
+ "socks5_errors": {
+ "connection_failed": 3,
+ "auth_failed": 1
+ }
+}
+```
+
+### 命令行监控
+
+```bash
+# 查看实时统计
+curl -s http://127.0.0.1:8080/stats | jq
+
+# 健康检查
+curl -s http://127.0.0.1:8080/health | jq
+
+# 监控脚本
+watch -n 5 'curl -s http://127.0.0.1:8080/health | jq .success_rate'
+```
+
+## 🔧 **故障排除**
+
+### 常见问题
+
+#### 1. 权限不足
+
+```
+❌ Failed to set system proxy: permission denied
+```
+
+**解决方案**:
+- macOS/Linux: 使用 `sudo` 运行
+- Windows: 以管理员身份运行
+
+#### 2. 代理设置失败
+
+```
+⚠️ You may need to run with administrator privileges
+📋 Manual setup instructions:
+ - Set HTTP proxy to: 127.0.0.1:8080
+ - Set HTTPS proxy to: 127.0.0.1:8080
+```
+
+**解决方案**: 按照提示手动配置系统代理设置
+
+#### 3. SOCKS5 连接失败
+
+```
+❌ Failed to connect via SOCKS5: connection refused
+```
+
+**解决方案**:
+- 检查 SOCKS5 服务器地址和端口
+- 验证用户名和密码
+- 确认网络连通性
+
+#### 4. DNS 解析问题
+
+```
+⚠️ Failed to start DNS proxy: bind: address already in use
+```
+
+**解决方案**:
+- 更改 DNS 代理端口: `dnsPort: 5354`
+- 停止占用端口的其他服务
+
+### 调试模式
+
+启用详细日志进行调试:
+
+```yaml
+# 配置文件中设置
+logLevel: debug
+
+# 或使用环境变量
+export LOG_LEVEL=debug
+```
+
+调试日志示例:
+
+```
+[DEBUG] Route matcher initialized with 15 bypass domains, 25 force domains
+[DEBUG] Using network service: Wi-Fi
+[DEBUG] Host www.google.com matches force domains - using proxy
+[DEBUG] Host www.baidu.com matches bypass domains - using direct
+[DEBUG] IP 192.168.1.1 is private - using direct
+```
+
+## 🛡️ **安全注意事项**
+
+### 1. 配置文件安全
+- 使用强密码
+- 限制配置文件访问权限: `chmod 600 configs/client.yaml`
+- 不要在公开仓库中提交包含密码的配置
+
+### 2. 网络安全
+- 使用可信的 SOCKS5 服务器
+- 启用 SSL 证书验证: `verifySSL: true`
+- 定期更新客户端版本
+
+### 3. 系统安全
+- 仅在需要时使用管理员权限
+- 定期检查系统代理设置
+- 程序退出时自动恢复原始设置
+
+## 📖 **使用场景**
+
+### 场景 1: 开发者环境
+
+```yaml
+# 适合开发者的配置
+globalProxy:
+ routing:
+ forceDomains:
+ - "*.github.com"
+ - "*.stackoverflow.com"
+ - "*.npmjs.com"
+ - "*.docker.com"
+ bypassDomains:
+ - "*.local"
+ - "localhost"
+```
+
+### 场景 2: 办公环境
+
+```yaml
+# 适合办公环境的配置
+globalProxy:
+ routing:
+ bypassLocal: true
+ bypassPrivate: true
+ forceDomains:
+ - "*.google.com"
+ - "*.zoom.us"
+ - "*.dropbox.com"
+```
+
+### 场景 3: 家庭使用
+
+```yaml
+# 适合家庭用户的配置
+globalProxy:
+ routing:
+ forceDomains:
+ - "*.youtube.com"
+ - "*.netflix.com"
+ - "*.twitter.com"
+ bypassDomains:
+ - "*.cn"
+ - "*.baidu.com"
+```
+
+## 🔄 **安全退出**
+
+全局代理支持优雅停止,按 `Ctrl+C` 时会:
+
+1. **停止 HTTP 服务器** (最多等待 5 秒)
+2. **恢复系统代理设置**
+3. **停止 DNS 代理**
+4. **显示最终统计信息**
+5. **清理所有资源**
+
+```
+🛑 Received shutdown signal...
+🔄 Shutting down gracefully...
+✅ HTTP server stopped
+🔄 Restoring system proxy settings...
+✅ System proxy restored successfully
+🔄 Stopping DNS proxy...
+✅ DNS proxy stopped
+📊 Final statistics:
+ - Total connections: 42
+ - Success rate: 95.24%
+ - Total bytes: 1048576
+ - Uptime: 15m30s
+✅ Cleanup completed
+```
+
+## 📚 **相关链接**
+
+- [API 文档](api.md)
+- [配置参考](../configs/client.yaml)
+- [故障排除](../README.md#故障排除)
+- [项目主页](../README.md)
\ No newline at end of file
diff --git a/internal/client/client.go b/internal/client/client.go
index a008de7..b4c4508 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -1,13 +1,17 @@
package client
import (
+ "context"
"fmt"
+ "net/http"
"os"
"os/signal"
"syscall"
+ "time"
"github.com/azoic/wormhole-client/internal/config"
"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"
@@ -19,12 +23,19 @@ type Client struct {
socks5Proxy *proxy.SOCKS5Proxy
dnsProxy *dns.DNSProxy
systemProxyMgr *system.SystemProxyManager
+ routeMatcher *routing.RouteMatcher
+ 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,
}
}
@@ -57,6 +68,16 @@ func (c *Client) Start(configPath string) error {
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")
+ }
+ }
+
// 设置信号处理
c.setupSignalHandler()
@@ -76,12 +97,14 @@ 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)
+ c.httpServer = 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)
+ 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)
- return server.ListenAndServe()
+ return c.httpServer.ListenAndServe()
}
func (c *Client) startGlobalProxy() error {
@@ -90,21 +113,29 @@ func (c *Client) startGlobalProxy() error {
logger.Info("⚠️ Requires administrator privileges")
// 启动HTTP代理服务器
- server := c.socks5Proxy.CreateHTTPProxy(c.config.Proxy.LocalPort)
+ c.httpServer = c.socks5Proxy.CreateHTTPProxy(c.config.Proxy.LocalPort)
+
+ errChan := make(chan error, 1)
go func() {
- if err := server.ListenAndServe(); err != nil {
- logger.Error("HTTP proxy server failed: %v", err)
+ 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: Set HTTP/HTTPS proxy to %s", httpProxy)
+ 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")
}
@@ -112,29 +143,55 @@ func (c *Client) startGlobalProxy() error {
// 启动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("🛑 Press Ctrl+C to stop")
- // 保持运行
- select {}
+ // 检查是否有错误
+ 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")
}
@@ -145,15 +202,29 @@ func (c *Client) setupSignalHandler() {
go func() {
<-sigChan
- logger.Info("🛑 Shutting down...")
- c.cleanup()
- os.Exit(0)
+ logger.Info("🛑 Received shutdown signal...")
+ c.shutdown()
}()
}
-func (c *Client) cleanup() {
+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 {
+ 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)
}
@@ -161,10 +232,58 @@ func (c *Client) cleanup() {
// 停止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
}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
new file mode 100644
index 0000000..60a41e1
--- /dev/null
+++ b/internal/config/config_test.go
@@ -0,0 +1,205 @@
+package config
+
+import (
+ "os"
+ "testing"
+ "time"
+)
+
+func TestLoadConfig(t *testing.T) {
+ // 创建临时配置文件
+ tmpFile, err := os.CreateTemp("", "test_config_*.yaml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmpFile.Name())
+
+ configContent := `
+serviceType: client
+server:
+ address: 127.0.0.1
+ port: 1080
+ username: testuser
+ password: testpass
+proxy:
+ mode: http
+ localPort: 8080
+globalProxy:
+ enabled: true
+ dnsProxy: true
+ dnsPort: 5353
+logLevel: debug
+timeout: 60s
+`
+
+ if _, err := tmpFile.WriteString(configContent); err != nil {
+ t.Fatal(err)
+ }
+ tmpFile.Close()
+
+ // 测试加载配置
+ config, err := LoadConfig(tmpFile.Name())
+ if err != nil {
+ t.Fatalf("LoadConfig failed: %v", err)
+ }
+
+ // 验证配置值
+ if config.ServiceType != "client" {
+ t.Errorf("Expected ServiceType 'client', got '%s'", config.ServiceType)
+ }
+
+ if config.Server.Address != "127.0.0.1" {
+ t.Errorf("Expected Server.Address '127.0.0.1', got '%s'", config.Server.Address)
+ }
+
+ if config.Server.Port != 1080 {
+ t.Errorf("Expected Server.Port 1080, got %d", config.Server.Port)
+ }
+
+ if config.Server.Username != "testuser" {
+ t.Errorf("Expected Server.Username 'testuser', got '%s'", config.Server.Username)
+ }
+
+ if config.Server.Password != "testpass" {
+ t.Errorf("Expected Server.Password 'testpass', got '%s'", config.Server.Password)
+ }
+
+ if config.Proxy.LocalPort != 8080 {
+ t.Errorf("Expected Proxy.LocalPort 8080, got %d", config.Proxy.LocalPort)
+ }
+
+ if config.LogLevel != "debug" {
+ t.Errorf("Expected LogLevel 'debug', got '%s'", config.LogLevel)
+ }
+
+ if config.Timeout != 60*time.Second {
+ t.Errorf("Expected Timeout 60s, got %v", config.Timeout)
+ }
+}
+
+func TestLoadConfigDefaults(t *testing.T) {
+ // 创建最小配置
+ tmpFile, err := os.CreateTemp("", "test_config_min_*.yaml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmpFile.Name())
+
+ minimalConfig := `
+serviceType: client
+server:
+ address: 127.0.0.1
+ port: 1080
+ username: user
+ password: pass
+`
+
+ if _, err := tmpFile.WriteString(minimalConfig); err != nil {
+ t.Fatal(err)
+ }
+ tmpFile.Close()
+
+ config, err := LoadConfig(tmpFile.Name())
+ if err != nil {
+ t.Fatalf("LoadConfig failed: %v", err)
+ }
+
+ // 验证默认值
+ if config.LogLevel != "info" {
+ t.Errorf("Expected default LogLevel 'info', got '%s'", config.LogLevel)
+ }
+
+ if config.Timeout != 30*time.Second {
+ t.Errorf("Expected default Timeout 30s, got %v", config.Timeout)
+ }
+
+ if config.Proxy.LocalPort != 8080 {
+ t.Errorf("Expected default Proxy.LocalPort 8080, got %d", config.Proxy.LocalPort)
+ }
+}
+
+func TestGetServerAddr(t *testing.T) {
+ config := &Config{
+ Server: Server{
+ Address: "example.com",
+ Port: 1080,
+ },
+ }
+
+ expected := "example.com:1080"
+ result := config.GetServerAddr()
+
+ if result != expected {
+ t.Errorf("Expected '%s', got '%s'", expected, result)
+ }
+}
+
+func TestValidate(t *testing.T) {
+ tests := []struct {
+ name string
+ config *Config
+ wantErr bool
+ }{
+ {
+ name: "valid config",
+ config: &Config{
+ Server: Server{
+ Address: "127.0.0.1",
+ Port: 1080,
+ },
+ Proxy: Proxy{
+ Mode: "http",
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "empty address",
+ config: &Config{
+ Server: Server{
+ Address: "",
+ Port: 1080,
+ },
+ Proxy: Proxy{
+ Mode: "http",
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "invalid port",
+ config: &Config{
+ Server: Server{
+ Address: "127.0.0.1",
+ Port: -1,
+ },
+ Proxy: Proxy{
+ Mode: "http",
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "invalid mode",
+ config: &Config{
+ Server: Server{
+ Address: "127.0.0.1",
+ Port: 1080,
+ },
+ Proxy: Proxy{
+ Mode: "invalid",
+ },
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := tt.config.Validate()
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/internal/proxy/socks5.go b/internal/proxy/socks5.go
index 3bfe663..e8a5d41 100644
--- a/internal/proxy/socks5.go
+++ b/internal/proxy/socks5.go
@@ -1,10 +1,14 @@
package proxy
import (
+ "context"
+ "encoding/json"
"fmt"
"io"
"net"
"net/http"
+ "strconv"
+ "sync"
"time"
"github.com/azoic/wormhole-client/pkg/logger"
@@ -16,6 +20,15 @@ type SOCKS5Proxy struct {
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代理客户端
@@ -25,6 +38,11 @@ func NewSOCKS5Proxy(serverAddr, username, password string, timeout time.Duration
username: username,
password: password,
timeout: timeout,
+ connPool: &connectionPool{
+ connections: make(chan net.Conn, 10),
+ maxSize: 10,
+ },
+ stats: NewProxyStats(),
}
}
@@ -34,9 +52,19 @@ func (p *SOCKS5Proxy) CreateHTTPProxy(localPort int) *http.Server {
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: proxyHandler,
+ 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
@@ -48,7 +76,11 @@ type httpProxyHandler struct {
}
func (h *httpProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- logger.Info("Processing request: %s %s", r.Method, r.URL.String())
+ // 统计连接
+ 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)
@@ -59,17 +91,25 @@ func (h *httpProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// handleHTTPSProxy 处理HTTPS代理请求 (CONNECT方法)
func (h *httpProxyHandler) handleHTTPSProxy(w http.ResponseWriter, r *http.Request) {
- destConn, err := h.socks5Proxy.DialTCP(r.Host)
+ ctx, cancel := context.WithTimeout(r.Context(), h.socks5Proxy.timeout)
+ defer cancel()
+
+ destConn, err := h.socks5Proxy.DialTCPWithContext(ctx, r.Host)
if err != nil {
- logger.Error("Failed to connect via SOCKS5: %v", err)
+ 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
@@ -77,22 +117,54 @@ func (h *httpProxyHandler) handleHTTPSProxy(w http.ResponseWriter, r *http.Reque
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)
+
// 双向数据转发
- go h.copyData(clientConn, destConn)
- h.copyData(destConn, clientConn)
+ 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.DialTCP(r.Host)
+ destConn, err := h.socks5Proxy.DialTCPWithContext(ctx, r.Host)
if err != nil {
- logger.Error("Failed to connect via SOCKS5: %v", err)
+ 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
}
@@ -100,56 +172,289 @@ func (h *httpProxyHandler) handleHTTPProxy(w http.ResponseWriter, r *http.Reques
// 发送HTTP请求
if err := r.Write(destConn); err != nil {
- logger.Error("Failed to write request: %v", err)
+ 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,
+ }
+
// 读取响应并返回给客户端
- if _, err := io.Copy(w, destConn); err != nil {
- logger.Error("Failed to copy response: %v", err)
+ 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代理服务器
- conn, err := net.DialTimeout("tcp", p.serverAddr, p.timeout)
+ 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 {
+ if err := conn.SetDeadline(deadline); err != nil {
+ conn.Close()
+ return nil, fmt.Errorf("failed to set deadline: %v", err)
+ }
+ }
+
// 执行SOCKS5握手
if err := p.performSOCKS5Handshake(conn, address); err != nil {
conn.Close()
return nil, fmt.Errorf("SOCKS5 handshake failed: %v", err)
}
+ // 清除deadline,让连接正常使用
+ if err := conn.SetDeadline(time.Time{}); err != nil {
+ logger.Debug("Failed to clear deadline: %v", err)
+ }
+
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 {
- // 简化的SOCKS5握手实现
- // 实际项目中应该完整实现SOCKS5协议
+ // 设置握手超时
+ deadline := time.Now().Add(p.timeout)
+ if err := conn.SetDeadline(deadline); err != nil {
+ return fmt.Errorf("failed to set handshake deadline: %v", err)
+ }
- // 发送认证方法选择
- authMethods := []byte{0x05, 0x01, 0x02} // 版本5,1个方法,用户名密码认证
+ // 第一步:发送认证方法选择
+ 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 := conn.Read(response); err != nil {
+ if _, err := io.ReadFull(conn, response); err != nil {
return fmt.Errorf("failed to read auth response: %v", err)
}
- if response[0] != 0x05 || response[1] != 0x02 {
- return fmt.Errorf("unsupported authentication method")
+ 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)))
@@ -163,55 +468,60 @@ func (p *SOCKS5Proxy) performSOCKS5Handshake(conn net.Conn, targetAddr string) e
// 读取认证结果
authResult := make([]byte, 2)
- if _, err := conn.Read(authResult); err != nil {
+ if _, err := io.ReadFull(conn, authResult); err != nil {
return fmt.Errorf("failed to read auth result: %v", err)
}
- if authResult[1] != 0x00 {
- return fmt.Errorf("authentication failed")
+ if authResult[0] != 0x01 {
+ return fmt.Errorf("invalid auth response version: %d", authResult[0])
}
- // 发送连接请求
- host, portStr, err := net.SplitHostPort(targetAddr)
- if err != nil {
- return fmt.Errorf("invalid target address: %v", err)
+ if authResult[1] != 0x00 {
+ return fmt.Errorf("authentication failed")
}
- // 简化的连接请求(实际实现应该支持域名解析)
- connectReq := []byte{0x05, 0x01, 0x00, 0x03} // 版本,连接命令,保留字段,域名类型
- connectReq = append(connectReq, byte(len(host)))
- connectReq = append(connectReq, []byte(host)...)
+ logger.Debug("SOCKS5 authentication successful")
+ return nil
+}
- // 添加端口
- portNum := 80 // 默认HTTP端口
- if portStr != "" {
- // 简化处理:如果端口是443则用443,否则用80
- if portStr == "443" {
- portNum = 443
- }
+// 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"
}
- connectReq = append(connectReq, byte(portNum>>8), byte(portNum&0xFF))
+}
- if _, err := conn.Write(connectReq); err != nil {
- return fmt.Errorf("failed to send connect request: %v", err)
+// parsePort 解析端口号
+func parsePort(portStr string) (int, error) {
+ if portStr == "" {
+ return 80, nil // 默认HTTP端口
}
- // 读取连接响应
- connectResp := make([]byte, 10) // 简化的响应读取
- if _, err := conn.Read(connectResp); err != nil {
- return fmt.Errorf("failed to read connect response: %v", err)
+ port, err := strconv.Atoi(portStr)
+ if err != nil {
+ return 0, fmt.Errorf("invalid port format: %s", portStr)
}
- if connectResp[1] != 0x00 {
- return fmt.Errorf("connection failed, status: %d", connectResp[1])
+ if port < 1 || port > 65535 {
+ return 0, fmt.Errorf("port out of range: %d", port)
}
- return nil
-}
-
-// copyData 数据复制
-func (h *httpProxyHandler) copyData(dst, src net.Conn) {
- defer dst.Close()
- defer src.Close()
- io.Copy(dst, src)
+ return port, nil
}
diff --git a/internal/proxy/socks5_test.go b/internal/proxy/socks5_test.go
new file mode 100644
index 0000000..46e76e2
--- /dev/null
+++ b/internal/proxy/socks5_test.go
@@ -0,0 +1,277 @@
+package proxy
+
+import (
+ "context"
+ "testing"
+ "time"
+)
+
+func TestNewSOCKS5Proxy(t *testing.T) {
+ proxy := NewSOCKS5Proxy("127.0.0.1:1080", "user", "pass", 30*time.Second)
+
+ if proxy == nil {
+ t.Fatal("NewSOCKS5Proxy returned nil")
+ }
+
+ if proxy.serverAddr != "127.0.0.1:1080" {
+ t.Errorf("Expected serverAddr '127.0.0.1:1080', got '%s'", proxy.serverAddr)
+ }
+
+ if proxy.username != "user" {
+ t.Errorf("Expected username 'user', got '%s'", proxy.username)
+ }
+
+ if proxy.password != "pass" {
+ t.Errorf("Expected password 'pass', got '%s'", proxy.password)
+ }
+
+ if proxy.timeout != 30*time.Second {
+ t.Errorf("Expected timeout 30s, got %v", proxy.timeout)
+ }
+
+ // 检查连接池是否正确初始化
+ if proxy.connPool == nil {
+ t.Error("Connection pool should be initialized")
+ }
+
+ if proxy.connPool.maxSize != 10 {
+ t.Errorf("Expected connection pool size 10, got %d", proxy.connPool.maxSize)
+ }
+}
+
+func TestParsePort(t *testing.T) {
+ tests := []struct {
+ input string
+ expected int
+ hasError bool
+ }{
+ {"80", 80, false},
+ {"443", 443, false},
+ {"8080", 8080, false},
+ {"", 80, false}, // 默认端口
+ {"0", 0, true}, // 无效端口
+ {"65536", 0, true}, // 端口超出范围
+ {"abc", 0, true}, // 非数字
+ {"-1", 0, true}, // 负数
+ {"65535", 65535, false}, // 最大有效端口
+ {"1", 1, false}, // 最小有效端口
+ }
+
+ for _, test := range tests {
+ result, err := parsePort(test.input)
+
+ if test.hasError {
+ if err == nil {
+ t.Errorf("Expected error for input '%s', but got none", test.input)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Unexpected error for input '%s': %v", test.input, err)
+ }
+ if result != test.expected {
+ t.Errorf("For input '%s', expected %d, got %d", test.input, test.expected, result)
+ }
+ }
+ }
+}
+
+func TestCreateHTTPProxy(t *testing.T) {
+ proxy := NewSOCKS5Proxy("127.0.0.1:1080", "user", "pass", 30*time.Second)
+ server := proxy.CreateHTTPProxy(8080)
+
+ if server == nil {
+ t.Fatal("CreateHTTPProxy returned nil")
+ }
+
+ if server.Addr != ":8080" {
+ t.Errorf("Expected server address ':8080', got '%s'", server.Addr)
+ }
+
+ // 检查服务器配置
+ if server.ReadTimeout != 30*time.Second {
+ t.Errorf("Expected ReadTimeout 30s, got %v", server.ReadTimeout)
+ }
+
+ if server.WriteTimeout != 30*time.Second {
+ t.Errorf("Expected WriteTimeout 30s, got %v", server.WriteTimeout)
+ }
+
+ if server.IdleTimeout != 120*time.Second {
+ t.Errorf("Expected IdleTimeout 120s, got %v", server.IdleTimeout)
+ }
+}
+
+func TestBuildConnectRequest(t *testing.T) {
+ proxy := NewSOCKS5Proxy("127.0.0.1:1080", "user", "pass", 30*time.Second)
+
+ tests := []struct {
+ name string
+ targetAddr string
+ expectError bool
+ expectedType byte // 地址类型: 1=IPv4, 3=域名, 4=IPv6
+ }{
+ {
+ name: "IPv4 address",
+ targetAddr: "192.168.1.1:80",
+ expectError: false,
+ expectedType: 0x01,
+ },
+ {
+ name: "IPv6 address",
+ targetAddr: "[2001:db8::1]:80",
+ expectError: false,
+ expectedType: 0x04,
+ },
+ {
+ name: "Domain name",
+ targetAddr: "example.com:80",
+ expectError: false,
+ expectedType: 0x03,
+ },
+ {
+ name: "Domain with HTTPS port",
+ targetAddr: "example.com:443",
+ expectError: false,
+ expectedType: 0x03,
+ },
+ {
+ name: "Invalid address format",
+ targetAddr: "invalid",
+ expectError: true,
+ },
+ {
+ name: "Invalid port",
+ targetAddr: "example.com:invalid",
+ expectError: true,
+ },
+ {
+ name: "Port out of range",
+ targetAddr: "example.com:70000",
+ expectError: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ req, err := proxy.buildConnectRequest(test.targetAddr)
+
+ if test.expectError {
+ if err == nil {
+ t.Errorf("Expected error for %s, but got none", test.targetAddr)
+ }
+ return
+ }
+
+ if err != nil {
+ t.Errorf("Unexpected error for %s: %v", test.targetAddr, err)
+ return
+ }
+
+ if len(req) < 4 {
+ t.Errorf("Request too short: %d bytes", len(req))
+ return
+ }
+
+ // 检查SOCKS版本
+ if req[0] != 0x05 {
+ t.Errorf("Expected SOCKS version 5, got %d", req[0])
+ }
+
+ // 检查命令类型
+ if req[1] != 0x01 {
+ t.Errorf("Expected CONNECT command (1), got %d", req[1])
+ }
+
+ // 检查保留字段
+ if req[2] != 0x00 {
+ t.Errorf("Expected reserved field to be 0, got %d", req[2])
+ }
+
+ // 检查地址类型
+ if req[3] != test.expectedType {
+ t.Errorf("Expected address type %d, got %d", test.expectedType, req[3])
+ }
+ })
+ }
+}
+
+func TestGetSOCKS5ErrorMessage(t *testing.T) {
+ tests := []struct {
+ code byte
+ expected string
+ }{
+ {0x01, "general SOCKS server failure"},
+ {0x02, "connection not allowed by ruleset"},
+ {0x03, "network unreachable"},
+ {0x04, "host unreachable"},
+ {0x05, "connection refused"},
+ {0x06, "TTL expired"},
+ {0x07, "command not supported"},
+ {0x08, "address type not supported"},
+ {0xFF, "unknown error"},
+ }
+
+ for _, test := range tests {
+ result := getSOCKS5ErrorMessage(test.code)
+ if result != test.expected {
+ t.Errorf("For code %d, expected '%s', got '%s'", test.code, test.expected, result)
+ }
+ }
+}
+
+func TestDialTCPWithContext(t *testing.T) {
+ proxy := NewSOCKS5Proxy("127.0.0.1:1080", "user", "pass", 5*time.Second)
+
+ // 测试超时上下文
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
+ defer cancel()
+
+ // 由于没有真实的SOCKS5服务器,这应该会超时或连接失败
+ _, err := proxy.DialTCPWithContext(ctx, "example.com:80")
+ if err == nil {
+ t.Error("Expected error when connecting to non-existent SOCKS5 server")
+ }
+}
+
+func TestHTTPProxyHandler(t *testing.T) {
+ proxy := NewSOCKS5Proxy("127.0.0.1:1080", "user", "pass", 30*time.Second)
+ handler := &httpProxyHandler{socks5Proxy: proxy}
+
+ if handler.socks5Proxy != proxy {
+ t.Error("Handler should have reference to SOCKS5 proxy")
+ }
+}
+
+// BenchmarkParsePort 性能测试
+func BenchmarkParsePort(b *testing.B) {
+ ports := []string{"80", "443", "8080", "3000", "9999"}
+
+ for i := 0; i < b.N; i++ {
+ port := ports[i%len(ports)]
+ _, err := parsePort(port)
+ if err != nil {
+ b.Errorf("Unexpected error: %v", err)
+ }
+ }
+}
+
+// TestIPv6AddressHandling 测试IPv6地址处理
+func TestIPv6AddressHandling(t *testing.T) {
+ proxy := NewSOCKS5Proxy("127.0.0.1:1080", "user", "pass", 30*time.Second)
+
+ // 测试完整的IPv6地址
+ req, err := proxy.buildConnectRequest("[2001:db8:85a3::8a2e:370:7334]:443")
+ if err != nil {
+ t.Fatalf("Failed to build connect request for IPv6: %v", err)
+ }
+
+ if req[3] != 0x04 {
+ t.Errorf("Expected IPv6 address type (4), got %d", req[3])
+ }
+
+ // IPv6地址应该是16字节 + 头部4字节 + 端口2字节 = 22字节
+ expectedLen := 4 + 16 + 2 // 头部 + IPv6地址 + 端口
+ if len(req) != expectedLen {
+ t.Errorf("Expected request length %d for IPv6, got %d", expectedLen, len(req))
+ }
+}
diff --git a/internal/proxy/stats.go b/internal/proxy/stats.go
new file mode 100644
index 0000000..406f6e6
--- /dev/null
+++ b/internal/proxy/stats.go
@@ -0,0 +1,121 @@
+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
+}
diff --git a/internal/proxy/stats_test.go b/internal/proxy/stats_test.go
new file mode 100644
index 0000000..af9e010
--- /dev/null
+++ b/internal/proxy/stats_test.go
@@ -0,0 +1,224 @@
+package proxy
+
+import (
+ "testing"
+ "time"
+)
+
+func TestNewProxyStats(t *testing.T) {
+ stats := NewProxyStats()
+
+ if stats == nil {
+ t.Fatal("NewProxyStats returned nil")
+ }
+
+ if stats.SOCKS5Errors == nil {
+ t.Error("SOCKS5Errors map should be initialized")
+ }
+
+ if time.Since(stats.StartTime) > time.Second {
+ t.Error("StartTime should be recent")
+ }
+}
+
+func TestProxyStatsCounters(t *testing.T) {
+ stats := NewProxyStats()
+
+ // 测试连接计数
+ stats.IncrementConnections()
+ stats.IncrementConnections()
+
+ snapshot := stats.GetStats()
+ if snapshot.TotalConnections != 2 {
+ t.Errorf("Expected 2 total connections, got %d", snapshot.TotalConnections)
+ }
+
+ if snapshot.ActiveConnections != 2 {
+ t.Errorf("Expected 2 active connections, got %d", snapshot.ActiveConnections)
+ }
+
+ // 测试减少活跃连接
+ stats.DecrementActiveConnections()
+ snapshot = stats.GetStats()
+ if snapshot.ActiveConnections != 1 {
+ t.Errorf("Expected 1 active connection, got %d", snapshot.ActiveConnections)
+ }
+
+ // 测试请求计数
+ stats.IncrementSuccessfulRequests()
+ stats.IncrementSuccessfulRequests()
+ stats.IncrementFailedRequests()
+
+ snapshot = stats.GetStats()
+ if snapshot.SuccessfulRequests != 2 {
+ t.Errorf("Expected 2 successful requests, got %d", snapshot.SuccessfulRequests)
+ }
+
+ if snapshot.FailedRequests != 1 {
+ t.Errorf("Expected 1 failed request, got %d", snapshot.FailedRequests)
+ }
+}
+
+func TestProxyStatsBytesTransferred(t *testing.T) {
+ stats := NewProxyStats()
+
+ stats.AddBytesTransferred(100, 200)
+ stats.AddBytesTransferred(50, 75)
+
+ snapshot := stats.GetStats()
+ if snapshot.BytesSent != 150 {
+ t.Errorf("Expected 150 bytes sent, got %d", snapshot.BytesSent)
+ }
+
+ if snapshot.BytesReceived != 275 {
+ t.Errorf("Expected 275 bytes received, got %d", snapshot.BytesReceived)
+ }
+
+ totalBytes := snapshot.GetTotalBytes()
+ if totalBytes != 425 {
+ t.Errorf("Expected 425 total bytes, got %d", totalBytes)
+ }
+}
+
+func TestProxyStatsSOCKS5Errors(t *testing.T) {
+ stats := NewProxyStats()
+
+ stats.IncrementSOCKS5Error("connection_failed")
+ stats.IncrementSOCKS5Error("auth_failed")
+ stats.IncrementSOCKS5Error("connection_failed")
+
+ snapshot := stats.GetStats()
+
+ if snapshot.SOCKS5Errors["connection_failed"] != 2 {
+ t.Errorf("Expected 2 connection_failed errors, got %d",
+ snapshot.SOCKS5Errors["connection_failed"])
+ }
+
+ if snapshot.SOCKS5Errors["auth_failed"] != 1 {
+ t.Errorf("Expected 1 auth_failed error, got %d",
+ snapshot.SOCKS5Errors["auth_failed"])
+ }
+}
+
+func TestProxyStatsSnapshot(t *testing.T) {
+ stats := NewProxyStats()
+
+ // 添加一些测试数据
+ stats.IncrementSuccessfulRequests()
+ stats.IncrementSuccessfulRequests()
+ stats.IncrementSuccessfulRequests()
+ stats.IncrementFailedRequests()
+
+ snapshot := stats.GetStats()
+
+ // 测试成功率计算
+ successRate := snapshot.GetSuccessRate()
+ expected := 75.0 // 3 successful out of 4 total = 75%
+ if successRate != expected {
+ t.Errorf("Expected success rate %.1f%%, got %.1f%%", expected, successRate)
+ }
+
+ // 测试零请求时的成功率
+ emptyStats := NewProxyStats()
+ emptySnapshot := emptyStats.GetStats()
+ if emptySnapshot.GetSuccessRate() != 0 {
+ t.Errorf("Expected 0%% success rate for empty stats, got %.1f%%",
+ emptySnapshot.GetSuccessRate())
+ }
+}
+
+func TestProxyStatsAverageConnections(t *testing.T) {
+ stats := NewProxyStats()
+
+ // 由于uptime很短,我们模拟一些连接
+ stats.IncrementConnections()
+ stats.IncrementConnections()
+
+ snapshot := stats.GetStats()
+ avg := snapshot.GetAverageConnectionsPerHour()
+
+ // 应该大于0,具体值取决于测试运行的时间
+ if avg <= 0 {
+ t.Error("Average connections per hour should be greater than 0")
+ }
+}
+
+func TestConcurrentStatsAccess(t *testing.T) {
+ stats := NewProxyStats()
+
+ // 并发测试
+ done := make(chan bool)
+
+ // 启动多个goroutine来并发更新统计
+ for i := 0; i < 10; i++ {
+ go func() {
+ for j := 0; j < 100; j++ {
+ stats.IncrementConnections()
+ stats.IncrementSuccessfulRequests()
+ stats.AddBytesTransferred(10, 20)
+ stats.IncrementSOCKS5Error("test_error")
+ }
+ done <- true
+ }()
+ }
+
+ // 等待所有goroutine完成
+ for i := 0; i < 10; i++ {
+ <-done
+ }
+
+ snapshot := stats.GetStats()
+
+ // 验证最终计数
+ expectedConnections := int64(1000) // 10 goroutines * 100 increments
+ if snapshot.TotalConnections != expectedConnections {
+ t.Errorf("Expected %d total connections, got %d",
+ expectedConnections, snapshot.TotalConnections)
+ }
+
+ if snapshot.SuccessfulRequests != expectedConnections {
+ t.Errorf("Expected %d successful requests, got %d",
+ expectedConnections, snapshot.SuccessfulRequests)
+ }
+
+ expectedBytesSent := int64(10000) // 10 * 100 * 10
+ if snapshot.BytesSent != expectedBytesSent {
+ t.Errorf("Expected %d bytes sent, got %d",
+ expectedBytesSent, snapshot.BytesSent)
+ }
+
+ if snapshot.SOCKS5Errors["test_error"] != expectedConnections {
+ t.Errorf("Expected %d test_error occurrences, got %d",
+ expectedConnections, snapshot.SOCKS5Errors["test_error"])
+ }
+}
+
+// BenchmarkStatsOperations 性能测试
+func BenchmarkStatsOperations(b *testing.B) {
+ stats := NewProxyStats()
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ stats.IncrementConnections()
+ stats.AddBytesTransferred(100, 200)
+ stats.IncrementSuccessfulRequests()
+ }
+}
+
+func BenchmarkStatsSnapshot(b *testing.B) {
+ stats := NewProxyStats()
+
+ // 添加一些数据
+ for i := 0; i < 100; i++ {
+ stats.IncrementConnections()
+ stats.AddBytesTransferred(100, 200)
+ stats.IncrementSOCKS5Error("test_error")
+ }
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ _ = stats.GetStats()
+ }
+}
diff --git a/internal/routing/matcher.go b/internal/routing/matcher.go
new file mode 100644
index 0000000..6235b55
--- /dev/null
+++ b/internal/routing/matcher.go
@@ -0,0 +1,263 @@
+package routing
+
+import (
+ "net"
+ "regexp"
+ "strings"
+
+ "github.com/azoic/wormhole-client/internal/config"
+ "github.com/azoic/wormhole-client/pkg/logger"
+)
+
+// RouteMatcher 路由匹配器
+type RouteMatcher struct {
+ config *config.Routing
+ bypassDomains []*regexp.Regexp
+ forceDomains []*regexp.Regexp
+ privateNetworks []*net.IPNet
+}
+
+// MatchResult 匹配结果
+type MatchResult int
+
+const (
+ // MatchBypass 直连(绕过代理)
+ MatchBypass MatchResult = iota
+ // MatchProxy 代理
+ MatchProxy
+ // MatchAuto 自动决定
+ MatchAuto
+)
+
+// NewRouteMatcher 创建路由匹配器
+func NewRouteMatcher(config *config.Routing) (*RouteMatcher, error) {
+ matcher := &RouteMatcher{
+ config: config,
+ }
+
+ // 编译域名规则
+ if err := matcher.compilePatterns(); err != nil {
+ return nil, err
+ }
+
+ // 初始化私有网络列表
+ matcher.initPrivateNetworks()
+
+ logger.Debug("Route matcher initialized with %d bypass domains, %d force domains",
+ len(matcher.bypassDomains), len(matcher.forceDomains))
+
+ return matcher, nil
+}
+
+// Match 匹配主机地址,返回路由决策
+func (rm *RouteMatcher) Match(host string) MatchResult {
+ // 去除端口
+ if hostOnly, _, err := net.SplitHostPort(host); err == nil {
+ host = hostOnly
+ }
+
+ logger.Debug("Matching route for host: %s", host)
+
+ // 1. 检查强制代理域名
+ if rm.matchesForceDomains(host) {
+ logger.Debug("Host %s matches force domains - using proxy", host)
+ return MatchProxy
+ }
+
+ // 2. 检查绕过域名
+ if rm.matchesBypassDomains(host) {
+ logger.Debug("Host %s matches bypass domains - using direct", host)
+ return MatchBypass
+ }
+
+ // 3. 检查是否为IP地址
+ if ip := net.ParseIP(host); ip != nil {
+ return rm.matchIP(ip)
+ }
+
+ // 4. 检查本地域名
+ if rm.config.BypassLocal && rm.isLocalDomain(host) {
+ logger.Debug("Host %s is local domain - using direct", host)
+ return MatchBypass
+ }
+
+ // 5. 默认策略:自动决定或代理
+ logger.Debug("Host %s no specific rule - using auto", host)
+ return MatchAuto
+}
+
+// matchesForceDomains 检查是否匹配强制代理域名
+func (rm *RouteMatcher) matchesForceDomains(host string) bool {
+ for _, pattern := range rm.forceDomains {
+ if pattern.MatchString(host) {
+ return true
+ }
+ }
+ return false
+}
+
+// matchesBypassDomains 检查是否匹配绕过域名
+func (rm *RouteMatcher) matchesBypassDomains(host string) bool {
+ for _, pattern := range rm.bypassDomains {
+ if pattern.MatchString(host) {
+ return true
+ }
+ }
+ return false
+}
+
+// matchIP 匹配IP地址
+func (rm *RouteMatcher) matchIP(ip net.IP) MatchResult {
+ // 检查本地IP
+ if rm.config.BypassLocal && rm.isLocalIP(ip) {
+ logger.Debug("IP %s is local - using direct", ip.String())
+ return MatchBypass
+ }
+
+ // 检查私有网络
+ if rm.config.BypassPrivate && rm.isPrivateIP(ip) {
+ logger.Debug("IP %s is private - using direct", ip.String())
+ return MatchBypass
+ }
+
+ return MatchAuto
+}
+
+// isLocalIP 检查是否为本地IP
+func (rm *RouteMatcher) isLocalIP(ip net.IP) bool {
+ // 环回地址
+ if ip.IsLoopback() {
+ return true
+ }
+
+ // 链路本地地址
+ if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
+ return true
+ }
+
+ return false
+}
+
+// isPrivateIP 检查是否为私有IP
+func (rm *RouteMatcher) isPrivateIP(ip net.IP) bool {
+ for _, network := range rm.privateNetworks {
+ if network.Contains(ip) {
+ return true
+ }
+ }
+ return false
+}
+
+// isLocalDomain 检查是否为本地域名
+func (rm *RouteMatcher) isLocalDomain(host string) bool {
+ host = strings.ToLower(host)
+
+ // 常见本地域名
+ localSuffixes := []string{
+ ".local",
+ ".localhost",
+ ".lan",
+ ".internal",
+ ".intranet",
+ ".home",
+ ".corp",
+ }
+
+ for _, suffix := range localSuffixes {
+ if strings.HasSuffix(host, suffix) {
+ return true
+ }
+ }
+
+ // 单词域名(无点)
+ if !strings.Contains(host, ".") {
+ return true
+ }
+
+ return false
+}
+
+// compilePatterns 编译域名匹配模式
+func (rm *RouteMatcher) compilePatterns() error {
+ // 编译绕过域名模式
+ for _, domain := range rm.config.BypassDomains {
+ pattern, err := rm.domainToRegexp(domain)
+ if err != nil {
+ return err
+ }
+ rm.bypassDomains = append(rm.bypassDomains, pattern)
+ }
+
+ // 编译强制代理域名模式
+ for _, domain := range rm.config.ForceDomains {
+ pattern, err := rm.domainToRegexp(domain)
+ if err != nil {
+ return err
+ }
+ rm.forceDomains = append(rm.forceDomains, pattern)
+ }
+
+ return nil
+}
+
+// domainToRegexp 将域名模式转换为正则表达式
+func (rm *RouteMatcher) domainToRegexp(domain string) (*regexp.Regexp, error) {
+ // 转义特殊字符
+ pattern := regexp.QuoteMeta(domain)
+
+ // 替换通配符
+ pattern = strings.ReplaceAll(pattern, "\\*", ".*")
+
+ // 添加行开始和结束标记
+ pattern = "^" + pattern + "$"
+
+ // 编译正则表达式(不区分大小写)
+ return regexp.Compile("(?i)" + pattern)
+}
+
+// initPrivateNetworks 初始化私有网络列表
+func (rm *RouteMatcher) initPrivateNetworks() {
+ privateNetworks := []string{
+ "10.0.0.0/8", // Class A private
+ "172.16.0.0/12", // Class B private
+ "192.168.0.0/16", // Class C private
+ "169.254.0.0/16", // Link-local
+ "127.0.0.0/8", // Loopback
+ "224.0.0.0/4", // Multicast
+ "240.0.0.0/4", // Reserved
+ "::1/128", // IPv6 loopback
+ "fe80::/10", // IPv6 link-local
+ "fc00::/7", // IPv6 unique local
+ }
+
+ for _, network := range privateNetworks {
+ if _, ipNet, err := net.ParseCIDR(network); err == nil {
+ rm.privateNetworks = append(rm.privateNetworks, ipNet)
+ }
+ }
+}
+
+// GetStats 获取路由统计信息
+func (rm *RouteMatcher) GetStats() map[string]interface{} {
+ return map[string]interface{}{
+ "bypass_domains_count": len(rm.bypassDomains),
+ "force_domains_count": len(rm.forceDomains),
+ "private_networks_count": len(rm.privateNetworks),
+ "bypass_local": rm.config.BypassLocal,
+ "bypass_private": rm.config.BypassPrivate,
+ }
+}
+
+// ReloadConfig 重新加载配置
+func (rm *RouteMatcher) ReloadConfig(config *config.Routing) error {
+ rm.config = config
+ rm.bypassDomains = nil
+ rm.forceDomains = nil
+
+ if err := rm.compilePatterns(); err != nil {
+ return err
+ }
+
+ logger.Info("Route matcher configuration reloaded")
+ return nil
+}
diff --git a/internal/routing/matcher_test.go b/internal/routing/matcher_test.go
new file mode 100644
index 0000000..a2cf862
--- /dev/null
+++ b/internal/routing/matcher_test.go
@@ -0,0 +1,329 @@
+package routing
+
+import (
+ "testing"
+
+ "github.com/azoic/wormhole-client/internal/config"
+)
+
+func TestNewRouteMatcher(t *testing.T) {
+ cfg := &config.Routing{
+ BypassLocal: true,
+ BypassPrivate: true,
+ BypassDomains: []string{"*.local", "*.lan"},
+ ForceDomains: []string{"*.google.com", "*.github.com"},
+ }
+
+ matcher, err := NewRouteMatcher(cfg)
+ if err != nil {
+ t.Fatalf("Failed to create route matcher: %v", err)
+ }
+
+ if matcher == nil {
+ t.Fatal("Route matcher should not be nil")
+ }
+
+ if len(matcher.bypassDomains) != 2 {
+ t.Errorf("Expected 2 bypass domains, got %d", len(matcher.bypassDomains))
+ }
+
+ if len(matcher.forceDomains) != 2 {
+ t.Errorf("Expected 2 force domains, got %d", len(matcher.forceDomains))
+ }
+}
+
+func TestRouteMatcher_Match(t *testing.T) {
+ cfg := &config.Routing{
+ BypassLocal: true,
+ BypassPrivate: true,
+ BypassDomains: []string{"*.local", "*.baidu.com"},
+ ForceDomains: []string{"*.google.com", "*.github.com"},
+ }
+
+ matcher, err := NewRouteMatcher(cfg)
+ if err != nil {
+ t.Fatalf("Failed to create route matcher: %v", err)
+ }
+
+ tests := []struct {
+ host string
+ expected MatchResult
+ }{
+ // 强制代理域名
+ {"www.google.com", MatchProxy},
+ {"api.github.com", MatchProxy},
+ {"www.google.com:443", MatchProxy},
+
+ // 绕过域名
+ {"test.local", MatchBypass},
+ {"www.baidu.com", MatchBypass},
+ {"search.baidu.com:80", MatchBypass},
+
+ // 本地域名
+ {"localhost", MatchBypass},
+ {"router.lan", MatchBypass},
+ {"printer.internal", MatchBypass},
+
+ // IP地址 - 本地
+ {"127.0.0.1", MatchBypass},
+ {"::1", MatchBypass},
+
+ // IP地址 - 私有网络
+ {"192.168.1.1", MatchBypass},
+ {"10.0.0.1", MatchBypass},
+ {"172.16.1.1", MatchBypass},
+
+ // 其他域名 - 自动决定
+ {"example.com", MatchAuto},
+ {"stackoverflow.com", MatchAuto},
+ }
+
+ for _, test := range tests {
+ t.Run(test.host, func(t *testing.T) {
+ result := matcher.Match(test.host)
+ if result != test.expected {
+ t.Errorf("For host %s, expected %v, got %v", test.host, test.expected, result)
+ }
+ })
+ }
+}
+
+func TestRouteMatcher_MatchesForceDomains(t *testing.T) {
+ cfg := &config.Routing{
+ ForceDomains: []string{"*.google.com", "github.com", "*.example.*"},
+ }
+
+ matcher, err := NewRouteMatcher(cfg)
+ if err != nil {
+ t.Fatalf("Failed to create route matcher: %v", err)
+ }
+
+ tests := []struct {
+ host string
+ expected bool
+ }{
+ {"www.google.com", true},
+ {"api.google.com", true},
+ {"google.com", false}, // 不匹配 *.google.com
+ {"github.com", true},
+ {"api.github.com", false}, // 不匹配 github.com
+ {"test.example.org", true},
+ {"sub.example.net", true},
+ {"example.com", false}, // 不匹配 *.example.*
+ {"other.com", false},
+ }
+
+ for _, test := range tests {
+ t.Run(test.host, func(t *testing.T) {
+ result := matcher.matchesForceDomains(test.host)
+ if result != test.expected {
+ t.Errorf("For host %s, expected %v, got %v", test.host, test.expected, result)
+ }
+ })
+ }
+}
+
+func TestRouteMatcher_MatchesBypassDomains(t *testing.T) {
+ cfg := &config.Routing{
+ BypassDomains: []string{"*.local", "localhost", "*.cn"},
+ }
+
+ matcher, err := NewRouteMatcher(cfg)
+ if err != nil {
+ t.Fatalf("Failed to create route matcher: %v", err)
+ }
+
+ tests := []struct {
+ host string
+ expected bool
+ }{
+ {"test.local", true},
+ {"printer.local", true},
+ {"local", false}, // 不匹配 *.local
+ {"localhost", true},
+ {"www.baidu.cn", true},
+ {"qq.cn", true},
+ {"china.com", false}, // 不匹配 *.cn
+ {"example.com", false},
+ }
+
+ for _, test := range tests {
+ t.Run(test.host, func(t *testing.T) {
+ result := matcher.matchesBypassDomains(test.host)
+ if result != test.expected {
+ t.Errorf("For host %s, expected %v, got %v", test.host, test.expected, result)
+ }
+ })
+ }
+}
+
+func TestRouteMatcher_IsLocalDomain(t *testing.T) {
+ cfg := &config.Routing{}
+ matcher, _ := NewRouteMatcher(cfg)
+
+ tests := []struct {
+ host string
+ expected bool
+ }{
+ {"localhost", true},
+ {"test.local", true},
+ {"printer.lan", true},
+ {"server.internal", true},
+ {"router.home", true},
+ {"pc.corp", true},
+ {"singleword", true}, // 单词域名
+ {"example.com", false},
+ {"www.google.com", false},
+ {"192.168.1.1", false}, // IP地址不是域名
+ }
+
+ for _, test := range tests {
+ t.Run(test.host, func(t *testing.T) {
+ result := matcher.isLocalDomain(test.host)
+ if result != test.expected {
+ t.Errorf("For host %s, expected %v, got %v", test.host, test.expected, result)
+ }
+ })
+ }
+}
+
+func TestRouteMatcher_IsPrivateIP(t *testing.T) {
+ cfg := &config.Routing{}
+ matcher, _ := NewRouteMatcher(cfg)
+
+ tests := []struct {
+ ip string
+ expected bool
+ }{
+ // 私有IPv4地址
+ {"192.168.1.1", true},
+ {"10.0.0.1", true},
+ {"172.16.1.1", true},
+ {"127.0.0.1", true}, // 环回地址
+
+ // 公网IPv4地址
+ {"8.8.8.8", false},
+ {"1.1.1.1", false},
+ {"114.114.114.114", false},
+
+ // IPv6地址
+ {"::1", true}, // 环回
+ {"fe80::1", true}, // 链路本地
+ {"fc00::1", true}, // 唯一本地
+ {"2001:db8::1", false}, // 公网(测试用)
+ }
+
+ for _, test := range tests {
+ t.Run(test.ip, func(t *testing.T) {
+ // 解析IP
+ ip := parseIPForTest(test.ip)
+ if ip == nil {
+ t.Fatalf("Failed to parse IP: %s", test.ip)
+ }
+
+ result := matcher.isPrivateIP(ip)
+ if result != test.expected {
+ t.Errorf("For IP %s, expected %v, got %v", test.ip, test.expected, result)
+ }
+ })
+ }
+}
+
+func TestRouteMatcher_GetStats(t *testing.T) {
+ cfg := &config.Routing{
+ BypassLocal: true,
+ BypassPrivate: false,
+ BypassDomains: []string{"*.local", "*.lan", "*.cn"},
+ ForceDomains: []string{"*.google.com", "*.github.com"},
+ }
+
+ matcher, err := NewRouteMatcher(cfg)
+ if err != nil {
+ t.Fatalf("Failed to create route matcher: %v", err)
+ }
+
+ stats := matcher.GetStats()
+
+ if stats["bypass_domains_count"] != 3 {
+ t.Errorf("Expected 3 bypass domains, got %v", stats["bypass_domains_count"])
+ }
+
+ if stats["force_domains_count"] != 2 {
+ t.Errorf("Expected 2 force domains, got %v", stats["force_domains_count"])
+ }
+
+ if stats["bypass_local"] != true {
+ t.Errorf("Expected bypass_local to be true, got %v", stats["bypass_local"])
+ }
+
+ if stats["bypass_private"] != false {
+ t.Errorf("Expected bypass_private to be false, got %v", stats["bypass_private"])
+ }
+}
+
+func TestDomainToRegexp(t *testing.T) {
+ cfg := &config.Routing{}
+ matcher, _ := NewRouteMatcher(cfg)
+
+ tests := []struct {
+ domain string
+ host string
+ matches bool
+ }{
+ {"*.google.com", "www.google.com", true},
+ {"*.google.com", "api.google.com", true},
+ {"*.google.com", "google.com", false},
+ {"github.com", "github.com", true},
+ {"github.com", "api.github.com", false},
+ {"*.example.*", "test.example.org", true},
+ {"*.example.*", "sub.example.net", true},
+ {"*.example.*", "example.com", false},
+ }
+
+ for _, test := range tests {
+ t.Run(test.domain+"->"+test.host, func(t *testing.T) {
+ pattern, err := matcher.domainToRegexp(test.domain)
+ if err != nil {
+ t.Fatalf("Failed to compile pattern %s: %v", test.domain, err)
+ }
+
+ matches := pattern.MatchString(test.host)
+ if matches != test.matches {
+ t.Errorf("Pattern %s against host %s: expected %v, got %v",
+ test.domain, test.host, test.matches, matches)
+ }
+ })
+ }
+}
+
+// 辅助函数
+func parseIPForTest(s string) []byte {
+ // 简单的IP解析用于测试
+ switch s {
+ case "192.168.1.1":
+ return []byte{192, 168, 1, 1}
+ case "10.0.0.1":
+ return []byte{10, 0, 0, 1}
+ case "172.16.1.1":
+ return []byte{172, 16, 1, 1}
+ case "127.0.0.1":
+ return []byte{127, 0, 0, 1}
+ case "8.8.8.8":
+ return []byte{8, 8, 8, 8}
+ case "1.1.1.1":
+ return []byte{1, 1, 1, 1}
+ case "114.114.114.114":
+ return []byte{114, 114, 114, 114}
+ // IPv6地址处理会更复杂,这里简化
+ case "::1":
+ return []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
+ case "fe80::1":
+ return []byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
+ case "fc00::1":
+ return []byte{0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
+ case "2001:db8::1":
+ return []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
+ default:
+ return nil
+ }
+}
diff --git a/internal/system/proxy.go b/internal/system/proxy.go
index 01f098f..85953e8 100644
--- a/internal/system/proxy.go
+++ b/internal/system/proxy.go
@@ -2,8 +2,10 @@ package system
import (
"fmt"
+ "os"
"os/exec"
"runtime"
+ "strconv"
"strings"
"github.com/azoic/wormhole-client/pkg/logger"
@@ -12,53 +14,102 @@ import (
// SystemProxyManager 系统代理管理器
type SystemProxyManager struct {
originalSettings map[string]string
+ isEnabled bool
}
// NewSystemProxyManager 创建系统代理管理器
func NewSystemProxyManager() *SystemProxyManager {
return &SystemProxyManager{
originalSettings: make(map[string]string),
+ isEnabled: false,
}
}
// SetGlobalProxy 设置全局代理
func (s *SystemProxyManager) SetGlobalProxy(httpProxy, httpsProxy, socksProxy string) error {
+ logger.Info("Setting system proxy...")
+ logger.Debug("HTTP: %s, HTTPS: %s, SOCKS: %s", httpProxy, httpsProxy, socksProxy)
+
+ var err error
switch runtime.GOOS {
case "darwin":
- return s.setMacOSProxy(httpProxy, httpsProxy, socksProxy)
+ err = s.setMacOSProxy(httpProxy, httpsProxy, socksProxy)
case "windows":
- return s.setWindowsProxy(httpProxy, httpsProxy, socksProxy)
+ err = s.setWindowsProxy(httpProxy, httpsProxy, socksProxy)
case "linux":
- return s.setLinuxProxy(httpProxy, httpsProxy, socksProxy)
+ err = s.setLinuxProxy(httpProxy, httpsProxy, socksProxy)
default:
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
+
+ if err == nil {
+ s.isEnabled = true
+ logger.Info("✅ System proxy configured successfully")
+ }
+
+ return err
}
// RestoreProxy 恢复原始代理设置
func (s *SystemProxyManager) RestoreProxy() error {
+ if !s.isEnabled {
+ logger.Debug("System proxy was not enabled, nothing to restore")
+ return nil
+ }
+
+ logger.Info("Restoring system proxy...")
+
+ var err error
switch runtime.GOOS {
case "darwin":
- return s.restoreMacOSProxy()
+ err = s.restoreMacOSProxy()
case "windows":
- return s.restoreWindowsProxy()
+ err = s.restoreWindowsProxy()
case "linux":
- return s.restoreLinuxProxy()
+ err = s.restoreLinuxProxy()
default:
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
+
+ if err == nil {
+ s.isEnabled = false
+ logger.Info("✅ System proxy restored successfully")
+ }
+
+ return err
+}
+
+// IsEnabled 检查代理是否已启用
+func (s *SystemProxyManager) IsEnabled() bool {
+ return s.isEnabled
+}
+
+// GetCurrentProxy 获取当前代理设置
+func (s *SystemProxyManager) GetCurrentProxy() (map[string]string, error) {
+ switch runtime.GOOS {
+ case "darwin":
+ return s.getMacOSCurrentProxy()
+ case "windows":
+ return s.getWindowsCurrentProxy()
+ case "linux":
+ return s.getLinuxCurrentProxy()
+ default:
+ return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
+ }
}
+// ===================== macOS 实现 =====================
+
// 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)
}
+ logger.Debug("Using network service: %s", networkService)
+
// 保存原始设置
if err := s.saveMacOSOriginalSettings(networkService); err != nil {
logger.Warn("Failed to save original proxy settings: %v", err)
@@ -66,41 +117,43 @@ func (s *SystemProxyManager) setMacOSProxy(httpProxy, httpsProxy, socksProxy str
// 设置HTTP代理
if httpProxy != "" {
- if err := s.runCommand("networksetup", "-setwebproxy", networkService,
- s.parseProxyHost(httpProxy), s.parseProxyPort(httpProxy)); err != nil {
+ host, port := s.parseProxyHostPort(httpProxy)
+ if err := s.runCommand("networksetup", "-setwebproxy", networkService, host, port); 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)
}
+ logger.Debug("HTTP proxy set to %s", httpProxy)
}
// 设置HTTPS代理
if httpsProxy != "" {
- if err := s.runCommand("networksetup", "-setsecurewebproxy", networkService,
- s.parseProxyHost(httpsProxy), s.parseProxyPort(httpsProxy)); err != nil {
+ host, port := s.parseProxyHostPort(httpsProxy)
+ if err := s.runCommand("networksetup", "-setsecurewebproxy", networkService, host, port); 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)
}
+ logger.Debug("HTTPS proxy set to %s", httpsProxy)
}
// 设置SOCKS代理
if socksProxy != "" {
- if err := s.runCommand("networksetup", "-setsocksfirewallproxy", networkService,
- s.parseProxyHost(socksProxy), s.parseProxyPort(socksProxy)); err != nil {
+ host, port := s.parseProxyHostPort(socksProxy)
+ if err := s.runCommand("networksetup", "-setsocksfirewallproxy", networkService, host, port); 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.Debug("SOCKS proxy set to %s", socksProxy)
}
- logger.Info("macOS system proxy configured successfully")
return nil
}
@@ -108,21 +161,34 @@ func (s *SystemProxyManager) setMacOSProxy(httpProxy, httpsProxy, socksProxy str
func (s *SystemProxyManager) getMacOSNetworkService() (string, error) {
output, err := exec.Command("networksetup", "-listallnetworkservices").Output()
if err != nil {
- return "", err
+ return "", fmt.Errorf("failed to list network services: %v", err)
}
lines := strings.Split(string(output), "\n")
+
+ // 优先查找Wi-Fi服务
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") {
+ lower := strings.ToLower(line)
+ if strings.Contains(lower, "wi-fi") || strings.Contains(lower, "wifi") {
return line, nil
}
}
}
- // 如果没找到Wi-Fi,返回第一个可用的服务
+ // 查找以太网服务
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+ if line != "" && !strings.HasPrefix(line, "*") && !strings.Contains(line, "An asterisk") {
+ lower := strings.ToLower(line)
+ if strings.Contains(lower, "ethernet") || strings.Contains(lower, "thunderbolt") {
+ return line, nil
+ }
+ }
+ }
+
+ // 返回第一个可用的服务
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "*") && !strings.Contains(line, "An asterisk") {
@@ -130,27 +196,25 @@ func (s *SystemProxyManager) getMacOSNetworkService() (string, error) {
}
}
- return "", fmt.Errorf("no network service found")
+ return "", fmt.Errorf("no active 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)
+ commands := map[string][]string{
+ "http_proxy": {"-getwebproxy", networkService},
+ "https_proxy": {"-getsecurewebproxy", networkService},
+ "socks_proxy": {"-getsocksfirewallproxy", networkService},
}
- // 保存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)
+ for key, args := range commands {
+ if output, err := exec.Command("networksetup", args...).Output(); err == nil {
+ s.originalSettings[key] = strings.TrimSpace(string(output))
+ }
}
s.originalSettings["network_service"] = networkService
+ logger.Debug("Saved original proxy settings for %s", networkService)
return nil
}
@@ -161,64 +225,333 @@ func (s *SystemProxyManager) restoreMacOSProxy() error {
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")
+ commands := [][]string{
+ {"-setwebproxystate", networkService, "off"},
+ {"-setsecurewebproxystate", networkService, "off"},
+ {"-setsocksfirewallproxystate", networkService, "off"},
+ }
+
+ for _, args := range commands {
+ if err := s.runCommand("networksetup", args...); err != nil {
+ logger.Warn("Failed to restore proxy setting: %v", err)
+ }
+ }
- logger.Info("macOS system proxy restored")
return nil
}
-// Windows 代理设置(简化实现)
+// 获取macOS当前代理设置
+func (s *SystemProxyManager) getMacOSCurrentProxy() (map[string]string, error) {
+ networkService, err := s.getMacOSNetworkService()
+ if err != nil {
+ return nil, err
+ }
+
+ result := make(map[string]string)
+
+ // 获取HTTP代理
+ if output, err := exec.Command("networksetup", "-getwebproxy", networkService).Output(); err == nil {
+ result["http"] = strings.TrimSpace(string(output))
+ }
+
+ // 获取HTTPS代理
+ if output, err := exec.Command("networksetup", "-getsecurewebproxy", networkService).Output(); err == nil {
+ result["https"] = strings.TrimSpace(string(output))
+ }
+
+ // 获取SOCKS代理
+ if output, err := exec.Command("networksetup", "-getsocksfirewallproxy", networkService).Output(); err == nil {
+ result["socks"] = strings.TrimSpace(string(output))
+ }
+
+ return result, nil
+}
+
+// ===================== Windows 实现 =====================
+
+// 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")
+ // 保存当前设置
+ if err := s.saveWindowsOriginalSettings(); err != nil {
+ logger.Warn("Failed to save Windows proxy settings: %v", err)
+ }
+
+ // 设置HTTP/HTTPS代理
+ if httpProxy != "" {
+ // 使用 netsh 或注册表设置代理
+ proxyServer := httpProxy
+ if httpsProxy != "" && httpsProxy != httpProxy {
+ proxyServer = fmt.Sprintf("http=%s;https=%s", httpProxy, httpsProxy)
+ }
+
+ // 使用PowerShell设置代理
+ cmd := fmt.Sprintf(`
+ $regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
+ Set-ItemProperty -Path $regPath -Name ProxyEnable -Value 1
+ Set-ItemProperty -Path $regPath -Name ProxyServer -Value "%s"
+ `, proxyServer)
+
+ if err := s.runPowerShell(cmd); err != nil {
+ return fmt.Errorf("failed to set Windows proxy: %v", err)
+ }
+
+ logger.Debug("Windows HTTP proxy set to %s", proxyServer)
+ }
+
+ return nil
}
-func (s *SystemProxyManager) restoreWindowsProxy() error {
- logger.Warn("Windows proxy restoration not fully implemented")
+// 保存Windows原始代理设置
+func (s *SystemProxyManager) saveWindowsOriginalSettings() error {
+ // 读取当前注册表设置
+ cmd := `
+ $regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
+ $enable = Get-ItemProperty -Path $regPath -Name ProxyEnable -ErrorAction SilentlyContinue
+ $server = Get-ItemProperty -Path $regPath -Name ProxyServer -ErrorAction SilentlyContinue
+ Write-Output "ProxyEnable:$($enable.ProxyEnable)"
+ Write-Output "ProxyServer:$($server.ProxyServer)"
+ `
+
+ output, err := s.runPowerShellWithOutput(cmd)
+ if err != nil {
+ return err
+ }
+
+ lines := strings.Split(output, "\n")
+ for _, line := range lines {
+ if strings.HasPrefix(line, "ProxyEnable:") {
+ s.originalSettings["windows_proxy_enable"] = strings.TrimPrefix(line, "ProxyEnable:")
+ } else if strings.HasPrefix(line, "ProxyServer:") {
+ s.originalSettings["windows_proxy_server"] = strings.TrimPrefix(line, "ProxyServer:")
+ }
+ }
+
return nil
}
-// Linux 代理设置(简化实现)
+// 恢复Windows代理设置
+func (s *SystemProxyManager) restoreWindowsProxy() error {
+ enable := s.originalSettings["windows_proxy_enable"]
+ server := s.originalSettings["windows_proxy_server"]
+
+ cmd := fmt.Sprintf(`
+ $regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
+ Set-ItemProperty -Path $regPath -Name ProxyEnable -Value %s
+ Set-ItemProperty -Path $regPath -Name ProxyServer -Value "%s"
+ `, strings.TrimSpace(enable), strings.TrimSpace(server))
+
+ return s.runPowerShell(cmd)
+}
+
+// 获取Windows当前代理设置
+func (s *SystemProxyManager) getWindowsCurrentProxy() (map[string]string, error) {
+ cmd := `
+ $regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
+ $enable = Get-ItemProperty -Path $regPath -Name ProxyEnable -ErrorAction SilentlyContinue
+ $server = Get-ItemProperty -Path $regPath -Name ProxyServer -ErrorAction SilentlyContinue
+ Write-Output "ProxyEnable:$($enable.ProxyEnable)"
+ Write-Output "ProxyServer:$($server.ProxyServer)"
+ `
+
+ output, err := s.runPowerShellWithOutput(cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ result := make(map[string]string)
+ lines := strings.Split(output, "\n")
+ for _, line := range lines {
+ if strings.HasPrefix(line, "ProxyEnable:") {
+ result["enabled"] = strings.TrimSpace(strings.TrimPrefix(line, "ProxyEnable:"))
+ } else if strings.HasPrefix(line, "ProxyServer:") {
+ result["server"] = strings.TrimSpace(strings.TrimPrefix(line, "ProxyServer:"))
+ }
+ }
+
+ return result, nil
+}
+
+// ===================== Linux 实现 =====================
+
+// 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")
+ // 保存当前环境变量
+ s.saveLinuxOriginalSettings()
+
+ envVars := []string{}
+
+ if httpProxy != "" {
+ envVars = append(envVars, fmt.Sprintf("export http_proxy=%s", httpProxy))
+ envVars = append(envVars, fmt.Sprintf("export HTTP_PROXY=%s", httpProxy))
+ }
+
+ if httpsProxy != "" {
+ envVars = append(envVars, fmt.Sprintf("export https_proxy=%s", httpsProxy))
+ envVars = append(envVars, fmt.Sprintf("export HTTPS_PROXY=%s", httpsProxy))
+ }
+
+ if socksProxy != "" {
+ envVars = append(envVars, fmt.Sprintf("export socks_proxy=%s", socksProxy))
+ envVars = append(envVars, fmt.Sprintf("export SOCKS_PROXY=%s", socksProxy))
+ }
+
+ // 写入到 /etc/environment (需要root权限)
+ envContent := strings.Join(envVars, "\n") + "\n"
+
+ // 尝试写入系统环境变量文件
+ if err := s.writeLinuxSystemProxy(envContent); err != nil {
+ logger.Warn("Failed to write system proxy settings: %v", err)
+ logger.Info("Please manually add the following to your shell profile:")
+ for _, env := range envVars {
+ logger.Info(" %s", env)
+ }
+ }
+
+ // 设置当前会话的环境变量
+ for _, env := range envVars {
+ parts := strings.SplitN(strings.TrimPrefix(env, "export "), "=", 2)
+ if len(parts) == 2 {
+ os.Setenv(parts[0], parts[1])
+ }
+ }
+
+ return nil
+}
+
+// 保存Linux原始代理设置
+func (s *SystemProxyManager) saveLinuxOriginalSettings() {
+ envVars := []string{"http_proxy", "https_proxy", "socks_proxy", "HTTP_PROXY", "HTTPS_PROXY", "SOCKS_PROXY"}
+
+ for _, env := range envVars {
+ if value := os.Getenv(env); value != "" {
+ s.originalSettings["linux_"+env] = value
+ }
+ }
}
+// 恢复Linux代理设置
func (s *SystemProxyManager) restoreLinuxProxy() error {
- logger.Warn("Linux proxy restoration not fully implemented")
+ // 清除当前会话的环境变量
+ envVars := []string{"http_proxy", "https_proxy", "socks_proxy", "HTTP_PROXY", "HTTPS_PROXY", "SOCKS_PROXY"}
+
+ for _, env := range envVars {
+ originalKey := "linux_" + env
+ if originalValue, exists := s.originalSettings[originalKey]; exists {
+ os.Setenv(env, originalValue)
+ } else {
+ os.Unsetenv(env)
+ }
+ }
+
+ logger.Info("Environment variables restored (session only)")
+ logger.Info("Note: You may need to manually remove proxy settings from system files")
+
return nil
}
-// 辅助函数
+// 获取Linux当前代理设置
+func (s *SystemProxyManager) getLinuxCurrentProxy() (map[string]string, error) {
+ result := make(map[string]string)
+
+ envVars := map[string]string{
+ "http": "http_proxy",
+ "https": "https_proxy",
+ "socks": "socks_proxy",
+ }
+
+ for key, env := range envVars {
+ if value := os.Getenv(env); value != "" {
+ result[key] = value
+ } else if value := os.Getenv(strings.ToUpper(env)); value != "" {
+ result[key] = value
+ }
+ }
+
+ return result, 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 fmt.Errorf("command '%s %s' failed: %v", name, strings.Join(args, " "), err)
}
+ logger.Debug("Command succeeded: %s %v", name, args)
return nil
}
-func (s *SystemProxyManager) parseProxyHost(proxy string) string {
- // 简单解析,格式: host:port
- parts := strings.Split(proxy, ":")
- if len(parts) >= 1 {
- return parts[0]
+// 运行PowerShell命令 (Windows)
+func (s *SystemProxyManager) runPowerShell(script string) error {
+ cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ logger.Error("PowerShell command failed: %s, output: %s", script, string(output))
+ return fmt.Errorf("PowerShell command failed: %v", err)
}
- return proxy
+ return nil
}
-func (s *SystemProxyManager) parseProxyPort(proxy string) string {
- // 简单解析,格式: host:port
+// 运行PowerShell命令并获取输出 (Windows)
+func (s *SystemProxyManager) runPowerShellWithOutput(script string) (string, error) {
+ cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
+ output, err := cmd.Output()
+ if err != nil {
+ return "", fmt.Errorf("PowerShell command failed: %v", err)
+ }
+ return string(output), nil
+}
+
+// 写入Linux系统代理设置
+func (s *SystemProxyManager) writeLinuxSystemProxy(content string) error {
+ // 尝试写入 /etc/environment
+ envFile := "/etc/environment"
+
+ // 检查是否有写权限
+ if _, err := os.Stat(envFile); os.IsNotExist(err) {
+ return fmt.Errorf("file %s does not exist", envFile)
+ }
+
+ // 备份原文件
+ if err := exec.Command("cp", envFile, envFile+".wormhole.backup").Run(); err != nil {
+ logger.Warn("Failed to backup %s: %v", envFile, err)
+ }
+
+ // 追加代理设置
+ f, err := os.OpenFile(envFile, os.O_APPEND|os.O_WRONLY, 0644)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ _, err = f.WriteString("\n# Wormhole SOCKS5 Client Proxy Settings\n" + content)
+ return err
+}
+
+// 解析代理地址
+func (s *SystemProxyManager) parseProxyHostPort(proxy string) (host, port string) {
+ // 移除协议前缀
+ proxy = strings.TrimPrefix(proxy, "http://")
+ proxy = strings.TrimPrefix(proxy, "https://")
+ proxy = strings.TrimPrefix(proxy, "socks5://")
+
parts := strings.Split(proxy, ":")
if len(parts) >= 2 {
- return parts[1]
+ host = parts[0]
+ port = parts[1]
+ } else {
+ host = proxy
+ port = "8080" // 默认端口
+ }
+
+ // 验证端口
+ if _, err := strconv.Atoi(port); err != nil {
+ port = "8080"
}
- return "8080" // 默认端口
+
+ return host, port
}