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 -[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://go.dev/) -[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen.svg)]() +[![Go Version](https://img.shields.io/badge/Go-1.19+-blue.svg)](https://golang.org) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen.svg)](.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 @@ + + + + + + wormhole-client: Go Coverage Report + + + +
+ +
+ not tracked + + no coverage + low coverage + * + * + * + * + * + * + * + * + high coverage + +
+
+
+ + + + + + + + + + + + + + + + + +
+ + + 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 }