添加全局代理

main
huyinsong 2 weeks ago
parent 6bad37115b
commit 4c0aeeb96d
  1. 2
      .gitignore
  2. 55
      .golangci.yml
  3. 21
      LICENSE
  4. 33
      Makefile
  5. 459
      README.md
  6. 178
      configs/client.yaml
  7. 1693
      coverage.html
  8. 231
      docs/api.md
  9. 379
      docs/global-proxy.md
  10. 147
      internal/client/client.go
  11. 205
      internal/config/config_test.go
  12. 420
      internal/proxy/socks5.go
  13. 277
      internal/proxy/socks5_test.go
  14. 121
      internal/proxy/stats.go
  15. 224
      internal/proxy/stats_test.go
  16. 263
      internal/routing/matcher.go
  17. 329
      internal/routing/matcher_test.go
  18. 449
      internal/system/proxy.go

2
.gitignore vendored

@ -37,7 +37,7 @@ go.work
.Trashes
ehthumbs.db
Thumbs.db
.github/
# Logs
*.log
logs/

@ -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

@ -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.

@ -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"

@ -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!**

@ -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"

File diff suppressed because it is too large Load Diff

@ -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
```

@ -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)

@ -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
}

@ -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)
}
})
}
}

@ -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
}

@ -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))
}
}

@ -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
}

@ -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()
}
}

@ -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
}

@ -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
}
}

@ -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
}

Loading…
Cancel
Save