Compare commits

...

3 Commits

  1. 4
      Makefile
  2. 6
      README.md
  3. 63
      configs/client.yaml
  4. 176
      docs/gui-features.md
  5. 353
      docs/gui.md
  6. 417
      internal/client/client.go
  7. 265
      internal/gui/server.go
  8. 843
      internal/gui/templates.go
  9. 25
      internal/proxy/socks5.go
  10. 57
      scripts/demo-gui.sh

@ -73,3 +73,7 @@ help:
@echo " deps - Download dependencies"
@echo " install - Install to /usr/local/bin"
@echo " uninstall - Remove from /usr/local/bin"
@echo ""
@echo "GUI Access:"
@echo " After starting, access the web GUI at:"
@echo " http://127.0.0.1:8080/gui"

@ -23,6 +23,7 @@
- 📈 **详细统计**: 连接数、成功率、流量统计、错误分类
- 💚 **健康检查**: RESTful API 端点,支持监控系统集成
- 🔍 **性能指标**: 响应时间、并发连接、字节传输统计
- 🖥 **Web GUI**: 现代化图形界面,实时监控和管理
### 🔒 **安全特性**
- 🛡 **认证支持**: 用户名密码认证和无认证模式
@ -52,7 +53,8 @@ make build
./bin/wormhole-client -mode http
# 配置浏览器代理: 127.0.0.1:8080
# 访问: http://127.0.0.1:8080/stats (查看统计)
# 访问 Web GUI: http://127.0.0.1:8080/gui
# 访问统计API: http://127.0.0.1:8080/stats
```
#### 2. 全局代理模式 (推荐进阶用户)
@ -191,6 +193,7 @@ routing:
启动后访问以下端点:
- 🖥 **Web GUI**: `http://127.0.0.1:8080/gui` (图形管理界面)
- 📊 **统计信息**: `http://127.0.0.1:8080/stats`
- 💚 **健康检查**: `http://127.0.0.1:8080/health`
- 🌐 **代理服务**: `http://127.0.0.1:8080/` (HTTP 代理入口)
@ -459,6 +462,7 @@ export LOG_LEVEL=debug
## 📚 **文档链接**
- 🖥 [Web GUI 使用指南](docs/gui.md) - 图形界面使用和功能介绍
- 📖 [全局代理使用指南](docs/global-proxy.md) - 详细的全局代理配置和使用说明
- 📊 [API 文档](docs/api.md) - RESTful API 接口说明
- 🔧 [配置参考](configs/client.yaml) - 完整的配置文件示例

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

@ -0,0 +1,176 @@
# Web GUI 功能特性
## 🎯 **功能概览**
Wormhole SOCKS5 Client 现已集成现代化的 Web GUI 管理界面,提供完整的图形化管理体验。
## ✨ **核心特性**
### 🖥 **现代化界面设计**
- **毛玻璃效果**: 使用 backdrop-filter 实现现代视觉效果
- **渐变背景**: 美观的紫色渐变背景设计
- **卡片布局**: 清晰的信息分组和层次结构
- **图标系统**: 集成 Font Awesome 图标库
- **响应式设计**: 支持桌面、平板、手机多端适配
### 📊 **实时监控面板**
- **连接统计**: 总连接数、活跃连接、成功率实时显示
- **流量监控**: 上传下载字节数、传输速度统计
- **性能指标**: 响应时间、运行时间、错误统计
- **自动刷新**: 每 5 秒自动更新数据,支持智能暂停
### 🎛 **代理控制中心**
- **一键开关**: 启用/禁用系统代理设置
- **状态显示**: 实时显示当前代理配置状态
- **配置信息**: 服务器地址、端口、模式等基本信息
- **操作反馈**: 详细的成功/错误提示信息
### 🛣 **智能路由管理**
- **域名规则**: 显示绕过和强制代理域名数量统计
- **网络配置**: 本地网络和私有网络绕过设置
- **规则统计**: 路由匹配规则的统计和分析
- **配置预览**: 当前路由配置的可视化展示
### 🔧 **系统信息中心**
- **代理状态**: 系统代理启用/禁用状态显示
- **配置详情**: 当前系统代理的具体配置信息
- **环境信息**: 操作系统、运行环境等系统信息
- **健康检查**: 服务健康状态和连通性检测
## 🚀 **技术特性**
### 🏗 **架构设计**
- **模块化设计**: GUI 模块独立,不影响核心代理功能
- **RESTful API**: 基于标准 REST API 构建,支持第三方集成
- **模板系统**: 使用 Go 内置模板系统,支持数据绑定
- **静态资源**: CSS/JS 内嵌,无需外部文件依赖
### 🔄 **实时更新机制**
- **WebSocket**: 考虑未来支持 WebSocket 实时推送
- **轮询更新**: 当前使用智能轮询机制,节省资源
- **页面可见性**: 支持页面可见性 API,后台时暂停更新
- **错误重试**: 网络错误时自动重试机制
### 📱 **响应式支持**
- **移动优先**: 移动端优化的触摸交互
- **自适应布局**: 基于 CSS Grid 的自适应布局系统
- **断点设计**: 768px/480px 断点,完美适配各种设备
- **触摸优化**: 更大的触摸目标和手势支持
### 🎨 **用户体验**
- **加载状态**: 操作时的加载动画和状态指示
- **状态反馈**: 颜色编码的状态指示器(绿/黄/红)
- **平滑动画**: CSS 过渡动画,提升交互体验
- **错误处理**: 友好的错误提示和处理机制
## 🔌 **API 接口**
### 📡 **状态 API**
```
GET /api/status - 获取服务运行状态
GET /api/system/proxy - 获取系统代理信息
GET /api/routing/stats - 获取路由统计信息
```
### ⚙ **控制 API**
```
POST /api/proxy/toggle - 切换系统代理状态
GET /api/config - 获取当前配置信息
```
### 🖥 **界面路由**
```
GET /gui - Web GUI 主界面
GET /static/style.css - 样式文件
GET /static/app.js - JavaScript 应用
```
## 🌐 **浏览器兼容性**
### ✅ **支持的浏览器**
- **Chrome/Chromium**: 70+ 版本
- **Firefox**: 65+ 版本
- **Safari**: 12+ 版本
- **Edge**: 79+ 版本(Chromium 内核)
### 🚫 **不支持的浏览器**
- Internet Explorer(所有版本)
- Chrome < 70
- Firefox < 65
- Safari < 12
## 🔒 **安全特性**
### 🛡 **访问控制**
- **本地绑定**: 默认只绑定 127.0.0.1,仅允许本地访问
- **无认证**: 当前版本无需登录(适合本地使用)
- **CORS 支持**: 支持跨域请求,便于 API 集成
### 🔐 **数据安全**
- **无敏感信息**: GUI 不显示密码等敏感配置
- **安全传输**: 建议通过 HTTPS 代理访问
- **最小权限**: GUI 只能查看状态,不能修改核心配置
## 📈 **性能优化**
### ⚡ **加载优化**
- **内嵌资源**: CSS/JS 内嵌,减少 HTTP 请求
- **缓存策略**: 静态资源缓存,提升加载速度
- **压缩优化**: 代码压缩和优化,减小传输体积
### 🔧 **运行优化**
- **异步操作**: 所有 API 调用均为异步,不阻塞界面
- **智能刷新**: 根据页面可见性智能控制刷新频率
- **内存管理**: 及时清理定时器和事件监听器
### 📊 **监控优化**
- **轻量级**: GUI 对系统资源占用极小
- **低延迟**: 本地通信,响应速度快
- **高可用**: 即使 GUI 异常也不影响代理功能
## 🔮 **未来规划**
### 🚧 **开发中功能**
- **配置编辑**: Web 界面直接编辑配置文件
- **日志查看**: 实时日志流和历史日志查看
- **性能图表**: 流量、连接数的图表可视化
- **规则管理**: 可视化的路由规则编辑器
### 💡 **计划功能**
- **多语言支持**: 英文、中文等多语言界面
- **主题切换**: 深色/浅色主题支持
- **用户认证**: 可选的用户认证和权限控制
- **WebSocket**: 实时数据推送,替代轮询
### 🎯 **长期目标**
- **移动 App**: 基于 Web 技术的移动应用
- **桌面客户端**: Electron 桌面应用
- **插件系统**: 支持第三方插件扩展
- **云端管理**: 多设备的云端统一管理
## 📞 **支持和反馈**
### 🐛 **问题报告**
如遇到 GUI 相关问题,请提供以下信息:
- 浏览器类型和版本
- 操作系统版本
- 错误截图或日志
- 复现步骤
### 💬 **功能建议**
欢迎提出 GUI 改进建议:
- 界面设计优化
- 功能需求建议
- 用户体验改进
- 性能优化建议
### 📖 **文档改进**
文档持续改进中,欢迎贡献:
- 使用指南完善
- 常见问题补充
- 最佳实践分享
- 多语言翻译
---
**🎉 感谢使用 Wormhole SOCKS5 Client Web GUI!**

@ -0,0 +1,353 @@
# Web GUI 使用指南
## 概述
Wormhole SOCKS5 Client 内置了现代化的 Web GUI 管理界面,提供直观的图形界面来管理和监控代理服务。
## 🌟 **主要功能**
### ✅ **实时监控**
- **连接统计**: 总连接数、活跃连接、成功率等
- **流量统计**: 上传下载流量、传输字节数
- **运行状态**: 服务运行时间、健康状态
### ✅ **代理控制**
- **一键开关**: 启用/禁用系统代理设置
- **状态显示**: 实时显示代理配置状态
- **错误提示**: 详细的操作反馈和错误信息
### ✅ **路由配置**
- **域名规则**: 显示绕过和强制代理域名数量
- **网络设置**: 本地网络和私有网络绕过设置
- **统计信息**: 路由规则匹配统计
### ✅ **系统信息**
- **系统代理**: 当前系统代理配置
- **服务配置**: SOCKS5 服务器地址、端口等
- **操作系统**: 运行环境信息
## 🚀 **快速开始**
### 1. 启动服务
```bash
# HTTP 代理模式(推荐测试)
./bin/wormhole-client -mode http
# 全局代理模式(推荐使用)
sudo ./bin/wormhole-client -mode global
```
### 2. 访问 GUI
打开浏览器访问:
- **主界面**: http://127.0.0.1:8080/gui
- **JSON API**: http://127.0.0.1:8080/stats
- **健康检查**: http://127.0.0.1:8080/health
### 3. 界面功能
启动后你将看到现代化的管理界面:
```
🌐 Wormhole SOCKS5 Client [●] 服务运行中
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 代理控制 │ │ 连接统计 │ │ 路由配置 │
│ │ │ │ │ │
│ 模式: global │ │ 总连接: 42 │ │ 绕过域名: 15 │
│ 服务器: xxx:xxx │ │ 活跃连接: 3 │ │ 强制域名: 25 │
│ 端口: 8080 │ │ 成功率: 95.2% │ │ 本地绕过: 是 │
│ 系统: darwin │ │ 运行时间: 2h15m │ │ 私网绕过: 是 │
│ │ │ 传输流量: 1.2GB │ │ │
│ [禁用系统代理] │ │ 失败请求: 2 │ │ │
│ [刷新状态] │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 系统信息 │
│ │
│ 系统代理状态: [已启用] │
│ 当前代理设置: │
│ http: 127.0.0.1:8080 │
│ https: 127.0.0.1:8080 │
└─────────────────────────────────────────────────────────────┘
```
## ⚙ **功能详解**
### 代理控制面板
**基本信息显示**:
- 运行模式(HTTP/Global/Transparent)
- SOCKS5 服务器地址和端口
- 本地代理端口
- 操作系统类型
**操作按钮**:
- **启用/禁用系统代理**: 一键切换系统代理设置
- **刷新状态**: 手动刷新所有数据
### 连接统计面板
**实时数据**:
- **总连接数**: 自启动以来的总连接数
- **活跃连接**: 当前正在处理的连接数
- **成功率**: 成功请求与总请求的比例
- **运行时间**: 服务运行的总时间
- **传输流量**: 上传下载的总字节数
- **失败请求**: 失败的请求数量
### 路由配置面板
**智能分流信息**:
- **绕过域名数量**: 配置的直连域名规则数
- **强制代理域名**: 配置的代理域名规则数
- **绕过本地网络**: 是否绕过本地网络设置
- **绕过私有网络**: 是否绕过私有网络设置
### 系统信息面板
**系统代理状态**:
- **启用/禁用状态**: 当前系统代理是否启用
- **代理设置详情**: 具体的 HTTP/HTTPS 代理配置
## 🔧 **高级配置**
### 自定义端口
如果需要在不同端口运行 GUI:
```yaml
# configs/client.yaml
proxy:
localPort: 8888 # 修改端口
# GUI 将在 http://127.0.0.1:8888/gui 提供服务
```
### 外部访问
默认 GUI 只能从本地访问。如需外部访问:
```bash
# 使用端口转发(SSH隧道)
ssh -L 8080:127.0.0.1:8080 username@your-server
# 然后在本地访问: http://127.0.0.1:8080/gui
```
### API 集成
GUI 基于 RESTful API 构建,可以与其他工具集成:
```bash
# 获取状态信息
curl http://127.0.0.1:8080/api/status
# 切换代理状态
curl -X POST http://127.0.0.1:8080/api/proxy/toggle \
-H "Content-Type: application/json" \
-d '{"enable": true}'
# 获取路由统计
curl http://127.0.0.1:8080/api/routing/stats
```
## 📱 **响应式设计**
GUI 支持多种设备:
- **桌面端**: 全功能四面板布局
- **平板端**: 自适应两列布局
- **手机端**: 单列垂直布局
### 移动端优化
在手机上访问时,界面会自动调整:
- 简化的按钮和控件
- 更大的触摸目标
- 优化的字体大小
## 🔄 **实时更新**
### 自动刷新
GUI 每 5 秒自动更新数据,包括:
- 连接统计
- 代理状态
- 系统信息
- 路由统计
### 智能刷新
**页面可见性检测**:
- 当页面在后台时停止自动刷新
- 切换回前台时立即恢复更新
- 节省系统资源和网络带宽
### 手动刷新
点击 "刷新状态" 按钮立即更新所有数据。
## 🎨 **界面特性**
### 现代化设计
- **毛玻璃效果**: 半透明背景增强视觉层次
- **渐变背景**: 美观的紫色渐变背景
- **卡片布局**: 清晰的信息分组
- **图标支持**: 使用 Font Awesome 图标
### 交互反馈
- **悬停效果**: 按钮和卡片的悬停动画
- **加载状态**: 操作进行时的加载指示
- **状态指示**: 颜色编码的状态点
- **动画效果**: 平滑的过渡动画
### 状态显示
**连接状态指示器**:
- 🟢 **绿色**: 服务正常运行
- 🟡 **黄色**: 正在检查状态
- 🔴 **红色**: 服务异常或连接失败
## ⚠ **注意事项**
### 安全考虑
1. **本地访问**: GUI 默认只绑定本地地址
2. **无认证**: 当前版本无需登录验证
3. **HTTPS**: 建议通过 HTTPS 代理访问
### 性能影响
1. **轻量级**: GUI 对系统资源占用很小
2. **异步更新**: 不影响代理性能
3. **缓存优化**: 静态资源缓存减少网络请求
### 浏览器兼容
**支持的浏览器**:
- ✅ Chrome/Chromium 70+
- ✅ Firefox 65+
- ✅ Safari 12+
- ✅ Edge 79+
**推荐浏览器**: Chrome 或 Firefox 最新版本
## 🔧 **故障排除**
### 常见问题
#### 1. 无法访问 GUI
```
问题: 浏览器显示"无法访问此网站"
解决: 检查服务是否启动,端口是否正确
```
```bash
# 检查服务状态
curl http://127.0.0.1:8080/health
# 检查端口占用
netstat -an | grep 8080
```
#### 2. 页面加载不完整
```
问题: 页面样式错乱或功能不正常
解决: 清除浏览器缓存,刷新页面
```
```bash
# 强制刷新页面: Ctrl+F5 (Windows) 或 Cmd+Shift+R (Mac)
```
#### 3. 代理切换失败
```
问题: 点击代理开关无效果
解决: 检查管理员权限,查看错误信息
```
```bash
# macOS/Linux: 使用 sudo 运行
sudo ./bin/wormhole-client -mode global
# Windows: 以管理员身份运行
```
#### 4. 数据不更新
```
问题: 统计数据停止更新
解决: 检查网络连接,手动刷新
```
### 调试模式
启用详细日志查看 GUI 相关信息:
```bash
# 设置日志级别为 debug
export LOG_LEVEL=debug
./bin/wormhole-client -mode global
```
查看 GUI 相关日志:
```
[DEBUG] GUI server initialized
[DEBUG] Serving GUI at /gui
[DEBUG] API request: GET /api/status
[DEBUG] GUI template rendered successfully
```
## 📚 **开发和定制**
### 模板系统
GUI 使用 Go 内置模板系统:
- **HTML 模板**: 存储在代码中,支持数据绑定
- **CSS 样式**: 响应式设计,支持深色主题
- **JavaScript**: 现代 ES6+ 语法,模块化设计
### API 端点
GUI 基于以下 API 端点:
```
GET /api/status - 获取服务状态
GET /api/config - 获取配置信息
POST /api/proxy/toggle - 切换代理状态
GET /api/routing/stats - 获取路由统计
GET /api/system/proxy - 获取系统代理信息
```
### 自定义样式
如需修改界面样式,可以编辑 `internal/gui/templates.go` 中的 CSS:
```go
// 修改主题色彩
.panel-header {
background: linear-gradient(135deg, #your-color1, #your-color2);
}
```
## 📖 **相关链接**
- 📊 [API 文档](api.md) - 详细的 API 接口说明
- 🌍 [全局代理指南](global-proxy.md) - 全局代理模式使用说明
- 📋 [配置参考](../configs/client.yaml) - 完整的配置文件示例
- 🏠 [项目主页](../README.md) - 项目总体介绍
---
**💡 提示**: GUI 界面会持续改进,欢迎提出建议和反馈!

@ -1,8 +1,12 @@
package client
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
@ -10,6 +14,7 @@ import (
"time"
"github.com/azoic/wormhole-client/internal/config"
"github.com/azoic/wormhole-client/internal/gui"
"github.com/azoic/wormhole-client/internal/proxy"
"github.com/azoic/wormhole-client/internal/routing"
"github.com/azoic/wormhole-client/internal/system"
@ -24,6 +29,7 @@ type Client struct {
dnsProxy *dns.DNSProxy
systemProxyMgr *system.SystemProxyManager
routeMatcher *routing.RouteMatcher
guiServer *gui.GUIServer
httpServer *http.Server
ctx context.Context
cancel context.CancelFunc
@ -78,6 +84,9 @@ func (c *Client) Start(configPath string) error {
}
}
// 创建 GUI 服务器
c.guiServer = gui.NewGUIServer(cfg, c.socks5Proxy, c.systemProxyMgr, c.routeMatcher, c.mode)
// 设置信号处理
c.setupSignalHandler()
@ -97,12 +106,13 @@ func (c *Client) startHTTPProxy() error {
logger.Info("🌐 Starting HTTP proxy mode...")
logger.Info("💡 Setting up HTTP proxy on port %d", c.config.Proxy.LocalPort)
c.httpServer = c.socks5Proxy.CreateHTTPProxy(c.config.Proxy.LocalPort)
c.httpServer = c.createHTTPServerWithGUI(c.config.Proxy.LocalPort)
logger.Info("✅ HTTP proxy started on :%d", c.config.Proxy.LocalPort)
logger.Info("💡 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)
logger.Info("🖥 Web GUI available at: http://127.0.0.1:%d/gui", c.config.Proxy.LocalPort)
return c.httpServer.ListenAndServe()
}
@ -113,7 +123,7 @@ func (c *Client) startGlobalProxy() error {
logger.Info("⚠ Requires administrator privileges")
// 启动HTTP代理服务器
c.httpServer = c.socks5Proxy.CreateHTTPProxy(c.config.Proxy.LocalPort)
c.httpServer = c.createHTTPServerWithGUI(c.config.Proxy.LocalPort)
errChan := make(chan error, 1)
go func() {
@ -171,6 +181,7 @@ func (c *Client) startGlobalProxy() error {
}
logger.Info("📊 Statistics: http://%s/stats", httpProxy)
logger.Info("💚 Health check: http://%s/health", httpProxy)
logger.Info("🖥 Web GUI: http://%s/gui", httpProxy)
logger.Info("🛑 Press Ctrl+C to stop")
// 检查是否有错误
@ -196,6 +207,408 @@ func (c *Client) startTransparentProxy() error {
return fmt.Errorf("transparent proxy mode not implemented")
}
// createHTTPServerWithGUI 创建带有 GUI 的 HTTP 服务器
func (c *Client) createHTTPServerWithGUI(localPort int) *http.Server {
// 创建ServeMux来处理不同的路径
mux := http.NewServeMux()
// 代理处理器
proxyHandler := &httpProxyHandler{
socks5Proxy: c.socks5Proxy,
routeMatcher: c.routeMatcher,
}
// API 路由
mux.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
stats := c.socks5Proxy.GetStats()
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
if err := json.NewEncoder(w).Encode(stats); err != nil {
logger.Error("Failed to encode stats: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
})
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
stats := c.socks5Proxy.GetStats()
health := map[string]interface{}{
"status": "healthy",
"uptime": stats.Uptime.String(),
"active_connections": stats.ActiveConnections,
"success_rate": stats.GetSuccessRate(),
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(health); err != nil {
logger.Error("Failed to encode health: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
})
// GUI 路由
if c.guiServer != nil {
c.guiServer.RegisterRoutes(mux)
}
// 创建一个包装器来处理CONNECT请求
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 对于CONNECT请求,直接使用代理处理器
if r.Method == http.MethodConnect {
proxyHandler.ServeHTTP(w, r)
return
}
// 对于其他请求,使用ServeMux
mux.ServeHTTP(w, r)
})
server := &http.Server{
Addr: fmt.Sprintf(":%d", localPort),
Handler: handler,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}
return server
}
// httpProxyHandler HTTP代理处理器
type httpProxyHandler struct {
socks5Proxy *proxy.SOCKS5Proxy
routeMatcher *routing.RouteMatcher
}
func (h *httpProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 添加调试日志
logger.Info("Received request: Method=%s, URL.Path=%s, Host=%s", r.Method, r.URL.Path, r.Host)
// 只对非CONNECT请求检查API和GUI路径
if r.Method != http.MethodConnect && isAPIOrGUIPath(r.URL.Path) {
logger.Info("Request blocked by isAPIOrGUIPath: %s", r.URL.Path)
http.NotFound(w, r)
return
}
// 统计连接 - 增加连接计数
h.socks5Proxy.IncrementConnections()
// 使用defer确保在请求结束时减少活跃连接数
defer h.socks5Proxy.DecrementActiveConnections()
// 调用代理处理逻辑
h.handleProxyRequest(w, r)
}
// handleProxyRequest 处理代理请求
func (h *httpProxyHandler) handleProxyRequest(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
h.handleHTTPSProxy(w, r)
} else {
h.handleHTTPProxy(w, r)
}
}
// handleHTTPSProxy 处理HTTPS代理请求 (CONNECT方法)
func (h *httpProxyHandler) handleHTTPSProxy(w http.ResponseWriter, r *http.Request) {
// 解析目标主机
host, port, err := net.SplitHostPort(r.Host)
if err != nil {
// 如果没有端口,默认使用443
host = r.Host
port = "443"
}
// 使用路由匹配器判断是否需要代理
if h.routeMatcher != nil {
matchResult := h.routeMatcher.Match(host)
switch matchResult {
case routing.MatchBypass:
// 直连
logger.Debug("HTTPS %s: Direct connection (bypass)", host)
h.handleDirectHTTPSProxy(w, r, host, port)
return
case routing.MatchProxy:
// 强制代理
logger.Debug("HTTPS %s: Using SOCKS5 proxy (force)", host)
case routing.MatchAuto:
// 自动决定,默认使用代理
logger.Debug("HTTPS %s: Using SOCKS5 proxy (auto)", host)
}
}
// 通过SOCKS5代理连接
targetHost := net.JoinHostPort(host, port)
destConn, err := h.socks5Proxy.DialTCP(targetHost)
if err != nil {
logger.Error("Failed to connect via SOCKS5 to %s: %v", targetHost, err)
h.socks5Proxy.IncrementFailedRequests()
http.Error(w, "Bad Gateway", http.StatusBadGateway)
return
}
defer destConn.Close()
// 发送200 Connection established响应
w.WriteHeader(http.StatusOK)
hijacker, ok := w.(http.Hijacker)
if !ok {
logger.Error("Hijacking not supported")
h.socks5Proxy.IncrementFailedRequests()
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
logger.Error("Failed to hijack connection: %v", err)
h.socks5Proxy.IncrementFailedRequests()
return
}
defer clientConn.Close()
// 记录成功建立连接
h.socks5Proxy.IncrementSuccessfulRequests()
// 双向数据转发
go func() {
defer destConn.Close()
defer clientConn.Close()
written, err := io.Copy(destConn, clientConn)
if err == nil && written > 0 {
h.socks5Proxy.AddBytesTransferred(written, 0)
}
}()
written, err := io.Copy(clientConn, destConn)
if err == nil && written > 0 {
h.socks5Proxy.AddBytesTransferred(0, written)
}
}
// handleDirectHTTPSProxy 直接连接处理HTTPS代理
func (h *httpProxyHandler) handleDirectHTTPSProxy(w http.ResponseWriter, r *http.Request, host, port string) {
// 直接连接到目标服务器
targetAddr := net.JoinHostPort(host, port)
destConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second)
if err != nil {
logger.Error("Failed to connect directly to %s: %v", targetAddr, err)
h.socks5Proxy.IncrementFailedRequests()
http.Error(w, "Bad Gateway", http.StatusBadGateway)
return
}
defer destConn.Close()
// 发送200 Connection established响应
w.WriteHeader(http.StatusOK)
hijacker, ok := w.(http.Hijacker)
if !ok {
logger.Error("Hijacking not supported")
h.socks5Proxy.IncrementFailedRequests()
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
logger.Error("Failed to hijack connection: %v", err)
h.socks5Proxy.IncrementFailedRequests()
return
}
defer clientConn.Close()
// 记录成功建立连接
h.socks5Proxy.IncrementSuccessfulRequests()
// 双向数据转发
go func() {
defer destConn.Close()
defer clientConn.Close()
written, err := io.Copy(destConn, clientConn)
if err == nil && written > 0 {
h.socks5Proxy.AddBytesTransferred(written, 0)
}
}()
written, err := io.Copy(clientConn, destConn)
if err == nil && written > 0 {
h.socks5Proxy.AddBytesTransferred(0, written)
}
}
// handleHTTPProxy 处理HTTP代理请求
func (h *httpProxyHandler) handleHTTPProxy(w http.ResponseWriter, r *http.Request) {
// 确保URL包含Host
if r.URL.Host == "" {
r.URL.Host = r.Host
}
if r.URL.Scheme == "" {
r.URL.Scheme = "http"
}
// 解析目标主机
host, port, err := net.SplitHostPort(r.Host)
if err != nil {
// 如果没有端口,根据协议添加默认端口
host = r.Host
if r.URL.Scheme == "https" {
port = "443"
} else {
port = "80"
}
}
// 使用路由匹配器判断是否需要代理
if h.routeMatcher != nil {
matchResult := h.routeMatcher.Match(host)
switch matchResult {
case routing.MatchBypass:
// 直连
logger.Debug("HTTP %s: Direct connection (bypass)", host)
h.handleDirectHTTPProxy(w, r)
return
case routing.MatchProxy:
// 强制代理
logger.Debug("HTTP %s: Using SOCKS5 proxy (force)", host)
case routing.MatchAuto:
// 自动决定,默认使用代理
logger.Debug("HTTP %s: Using SOCKS5 proxy (auto)", host)
}
}
// 通过SOCKS5代理连接
targetHost := net.JoinHostPort(host, port)
destConn, err := h.socks5Proxy.DialTCP(targetHost)
if err != nil {
logger.Error("Failed to connect via SOCKS5 to %s: %v", targetHost, err)
h.socks5Proxy.IncrementFailedRequests()
http.Error(w, "Bad Gateway", http.StatusBadGateway)
return
}
defer destConn.Close()
// 发送HTTP请求
if err := r.Write(destConn); err != nil {
logger.Error("Failed to write request to SOCKS5 connection: %v", err)
h.socks5Proxy.IncrementFailedRequests()
http.Error(w, "Bad Gateway", http.StatusBadGateway)
return
}
// 读取并转发响应
resp, err := http.ReadResponse(bufio.NewReader(destConn), r)
if err != nil {
logger.Error("Failed to read response from SOCKS5 connection: %v", err)
h.socks5Proxy.IncrementFailedRequests()
http.Error(w, "Bad Gateway", http.StatusBadGateway)
return
}
defer resp.Body.Close()
// 记录成功请求
h.socks5Proxy.IncrementSuccessfulRequests()
// 复制响应头
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
// 设置状态码
w.WriteHeader(resp.StatusCode)
// 复制响应体并统计字节数
written, err := io.Copy(w, resp.Body)
if err == nil && written > 0 {
h.socks5Proxy.AddBytesTransferred(0, written)
}
}
// handleDirectHTTPProxy 直接连接处理HTTP代理
func (h *httpProxyHandler) handleDirectHTTPProxy(w http.ResponseWriter, r *http.Request) {
// 创建直接连接的HTTP客户端
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
},
}
// 创建新的请求
newReq, err := http.NewRequest(r.Method, r.URL.String(), r.Body)
if err != nil {
logger.Error("Failed to create direct request: %v", err)
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
// 复制请求头
for key, values := range r.Header {
for _, value := range values {
newReq.Header.Add(key, value)
}
}
// 发送请求
resp, err := client.Do(newReq)
if err != nil {
logger.Error("Failed to make direct request: %v", err)
http.Error(w, "Bad Gateway", http.StatusBadGateway)
return
}
defer resp.Body.Close()
// 复制响应头
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
// 设置状态码
w.WriteHeader(resp.StatusCode)
// 复制响应体
if _, err := io.Copy(w, resp.Body); err != nil {
logger.Error("Failed to copy response body: %v", err)
return
}
}
// isAPIOrGUIPath 检查是否为API或GUI路径
func isAPIOrGUIPath(path string) bool {
apiPaths := []string{"/stats", "/health", "/api/", "/gui", "/static/"}
for _, apiPath := range apiPaths {
if path == apiPath || (apiPath[len(apiPath)-1] == '/' && len(path) > len(apiPath) && path[:len(apiPath)] == apiPath) {
return true
}
}
return false
}
func (c *Client) setupSignalHandler() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

@ -0,0 +1,265 @@
package gui
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"path/filepath"
"runtime"
"strings"
"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/logger"
)
// GUIServer Web GUI 服务器
type GUIServer struct {
config *config.Config
socks5Proxy *proxy.SOCKS5Proxy
systemProxyMgr *system.SystemProxyManager
routeMatcher *routing.RouteMatcher
mode string
templates *template.Template
}
// NewGUIServer 创建 GUI 服务器
func NewGUIServer(cfg *config.Config, socks5Proxy *proxy.SOCKS5Proxy,
systemProxyMgr *system.SystemProxyManager, routeMatcher *routing.RouteMatcher, mode string) *GUIServer {
gui := &GUIServer{
config: cfg,
socks5Proxy: socks5Proxy,
systemProxyMgr: systemProxyMgr,
routeMatcher: routeMatcher,
mode: mode,
}
// 加载模板
gui.loadTemplates()
return gui
}
// RegisterRoutes 注册 GUI 路由
func (g *GUIServer) RegisterRoutes(mux *http.ServeMux) {
// 静态文件路由
mux.HandleFunc("/static/", g.handleStatic)
// Web 界面路由
mux.HandleFunc("/gui", g.handleGUI)
mux.HandleFunc("/gui/", g.handleGUI)
// API 路由
mux.HandleFunc("/api/status", g.handleStatus)
mux.HandleFunc("/api/config", g.handleConfig)
mux.HandleFunc("/api/proxy/toggle", g.handleProxyToggle)
mux.HandleFunc("/api/routing/stats", g.handleRoutingStats)
mux.HandleFunc("/api/system/proxy", g.handleSystemProxy)
}
// handleGUI 处理 GUI 主页面
func (g *GUIServer) handleGUI(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
data := g.getTemplateData()
if err := g.templates.Execute(w, data); err != nil {
logger.Error("Failed to execute template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
// handleStatic 处理静态文件
func (g *GUIServer) handleStatic(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/static/")
var content string
var contentType string
switch filepath.Ext(path) {
case ".css":
content = g.getCSS()
contentType = "text/css"
case ".js":
content = g.getJS()
contentType = "application/javascript"
default:
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", contentType)
w.Write([]byte(content))
}
// handleStatus 处理状态 API
func (g *GUIServer) handleStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var proxyStats interface{}
if g.socks5Proxy != nil {
proxyStats = g.socks5Proxy.GetStats()
}
var routingStats interface{}
if g.routeMatcher != nil {
routingStats = g.routeMatcher.GetStats()
}
status := map[string]interface{}{
"mode": g.mode,
"system_proxy": g.systemProxyMgr.IsEnabled(),
"proxy_stats": proxyStats,
"routing_stats": routingStats,
"server_addr": g.config.GetServerAddr(),
"local_port": g.config.Proxy.LocalPort,
"os": runtime.GOOS,
}
g.writeJSON(w, status)
}
// handleConfig 处理配置 API
func (g *GUIServer) handleConfig(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
g.writeJSON(w, g.config)
case http.MethodPost:
// TODO: 实现配置更新
http.Error(w, "Configuration update not implemented", http.StatusNotImplemented)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleProxyToggle 处理代理开关
func (g *GUIServer) handleProxyToggle(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Enable bool `json:"enable"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if req.Enable && !g.systemProxyMgr.IsEnabled() {
// 启用系统代理
httpProxy := fmt.Sprintf("127.0.0.1:%d", g.config.Proxy.LocalPort)
if err := g.systemProxyMgr.SetGlobalProxy(httpProxy, httpProxy, ""); err != nil {
g.writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
})
return
}
} else if !req.Enable && g.systemProxyMgr.IsEnabled() {
// 禁用系统代理
if err := g.systemProxyMgr.RestoreProxy(); err != nil {
g.writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
})
return
}
}
g.writeJSON(w, map[string]interface{}{
"success": true,
"enabled": g.systemProxyMgr.IsEnabled(),
})
}
// handleRoutingStats 处理路由统计
func (g *GUIServer) handleRoutingStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var stats interface{}
if g.routeMatcher != nil {
stats = g.routeMatcher.GetStats()
}
g.writeJSON(w, stats)
}
// handleSystemProxy 处理系统代理信息
func (g *GUIServer) handleSystemProxy(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
currentProxy, err := g.systemProxyMgr.GetCurrentProxy()
if err != nil {
g.writeJSON(w, map[string]interface{}{
"error": err.Error(),
})
return
}
g.writeJSON(w, map[string]interface{}{
"enabled": g.systemProxyMgr.IsEnabled(),
"current_proxy": currentProxy,
})
}
// writeJSON 写入 JSON 响应
func (g *GUIServer) writeJSON(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
if err := json.NewEncoder(w).Encode(data); err != nil {
logger.Error("Failed to encode JSON: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
// getTemplateData 获取模板数据
func (g *GUIServer) getTemplateData() map[string]interface{} {
var proxyStats interface{}
if g.socks5Proxy != nil {
proxyStats = g.socks5Proxy.GetStats()
}
var routingStats interface{}
if g.routeMatcher != nil {
routingStats = g.routeMatcher.GetStats()
}
return map[string]interface{}{
"Title": "Wormhole SOCKS5 Client",
"Mode": g.mode,
"ServerAddr": g.config.GetServerAddr(),
"LocalPort": g.config.Proxy.LocalPort,
"SystemProxy": g.systemProxyMgr.IsEnabled(),
"ProxyStats": proxyStats,
"RoutingStats": routingStats,
"OS": runtime.GOOS,
}
}
// loadTemplates 加载模板
func (g *GUIServer) loadTemplates() {
g.templates = template.Must(template.New("index.html").Parse(g.getHTMLTemplate()))
}

@ -0,0 +1,843 @@
package gui
// getHTMLTemplate 返回 HTML 模板
func (g *GUIServer) getHTMLTemplate() string {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container">
<!-- 头部 -->
<header class="header">
<div class="header-content">
<div class="logo">
<i class="fas fa-globe"></i>
<h1>{{.Title}}</h1>
</div>
<div class="status-indicator">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">正在检查...</span>
</div>
</div>
</header>
<!-- 主要内容 -->
<main class="main-content">
<!-- 代理控制面板 -->
<section class="panel proxy-panel">
<div class="panel-header">
<h2><i class="fas fa-network-wired"></i> 代理控制</h2>
</div>
<div class="panel-content">
<div class="proxy-info">
<div class="info-item">
<label>运行模式:</label>
<span class="mode-badge" id="modeText">{{.Mode}}</span>
</div>
<div class="info-item">
<label>服务器地址:</label>
<span>{{.ServerAddr}}</span>
</div>
<div class="info-item">
<label>本地端口:</label>
<span>{{.LocalPort}}</span>
</div>
<div class="info-item">
<label>操作系统:</label>
<span>{{.OS}}</span>
</div>
</div>
<div class="proxy-controls">
<button id="toggleProxy" class="btn btn-primary">
<i class="fas fa-power-off"></i>
<span id="toggleText">启用系统代理</span>
</button>
<button id="refreshStatus" class="btn btn-secondary">
<i class="fas fa-sync-alt"></i>
刷新状态
</button>
</div>
</div>
</section>
<!-- 统计信息面板 -->
<section class="panel stats-panel">
<div class="panel-header">
<h2><i class="fas fa-chart-line"></i> 连接统计</h2>
</div>
<div class="panel-content">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" id="totalConnections">-</div>
<div class="stat-label">总连接数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="activeConnections">-</div>
<div class="stat-label">活跃连接</div>
</div>
<div class="stat-item">
<div class="stat-value" id="successRate">-</div>
<div class="stat-label">成功率</div>
</div>
<div class="stat-item">
<div class="stat-value" id="uptime">-</div>
<div class="stat-label">运行时间</div>
</div>
<div class="stat-item">
<div class="stat-value" id="bytesTransferred">-</div>
<div class="stat-label">传输流量</div>
</div>
<div class="stat-item">
<div class="stat-value" id="failedRequests">-</div>
<div class="stat-label">失败请求</div>
</div>
</div>
</div>
</section>
<!-- 路由配置面板 -->
<section class="panel routing-panel">
<div class="panel-header">
<h2><i class="fas fa-route"></i> 路由配置</h2>
</div>
<div class="panel-content">
<div class="routing-stats">
<div class="routing-item">
<label>绕过域名数量:</label>
<span id="bypassDomainsCount">-</span>
</div>
<div class="routing-item">
<label>强制代理域名:</label>
<span id="forceDomainsCount">-</span>
</div>
<div class="routing-item">
<label>绕过本地网络:</label>
<span id="bypassLocal">-</span>
</div>
<div class="routing-item">
<label>绕过私有网络:</label>
<span id="bypassPrivate">-</span>
</div>
</div>
</div>
</section>
<!-- 系统信息面板 -->
<section class="panel system-panel">
<div class="panel-header">
<h2><i class="fas fa-cog"></i> 系统信息</h2>
</div>
<div class="panel-content">
<div class="system-info">
<div class="info-item">
<label>系统代理状态:</label>
<span id="systemProxyStatus" class="status-badge">检查中...</span>
</div>
<div class="info-item">
<label>当前代理设置:</label>
<div id="currentProxySettings">-</div>
</div>
</div>
</div>
</section>
</main>
<!-- 底部 -->
<footer class="footer">
<p>&copy; 2024 Wormhole SOCKS5 Client | <a href="/stats" target="_blank">JSON API</a> |
<a href="/health" target="_blank">健康检查</a></p>
</footer>
</div>
<!-- 加载脚本 -->
<script src="/static/app.js"></script>
</body>
</html>`
}
// getCSS 返回 CSS 样式
func (g *GUIServer) getCSS() string {
return `
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
.header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
margin-bottom: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 15px;
}
.logo i {
font-size: 2.5em;
color: #667eea;
}
.logo h1 {
font-size: 2em;
color: #333;
font-weight: 600;
}
.status-indicator {
display: flex;
align-items: center;
gap: 10px;
font-weight: 500;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ffd700;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #00ff00;
}
.status-dot.disconnected {
background: #ff4444;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* 主要内容样式 */
.main-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 25px;
margin-bottom: 30px;
}
/* 面板样式 */
.panel {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.panel:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.panel-header {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 20px;
}
.panel-header h2 {
font-size: 1.3em;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.panel-content {
padding: 25px;
}
/* 代理面板样式 */
.proxy-info {
margin-bottom: 25px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.info-item:last-child {
border-bottom: none;
}
.info-item label {
font-weight: 600;
color: #555;
}
.mode-badge {
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
}
.proxy-controls {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
/* 按钮样式 */
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
min-width: 140px;
justify-content: center;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.btn-primary:hover {
background: linear-gradient(135deg, #5a6fd8, #6a4190);
transform: translateY(-2px);
}
.btn-secondary {
background: #f8f9fa;
color: #333;
border: 2px solid #dee2e6;
}
.btn-secondary:hover {
background: #e9ecef;
border-color: #adb5bd;
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
/* 统计面板样式 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.stat-item {
text-align: center;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
transition: transform 0.3s ease;
}
.stat-item:hover {
transform: scale(1.05);
}
.stat-value {
font-size: 2em;
font-weight: 700;
color: #667eea;
margin-bottom: 8px;
}
.stat-label {
font-size: 0.9em;
color: #666;
font-weight: 500;
}
/* 路由面板样式 */
.routing-stats {
display: flex;
flex-direction: column;
gap: 15px;
}
.routing-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.routing-item label {
font-weight: 600;
color: #555;
}
/* 系统面板样式 */
.system-info {
display: flex;
flex-direction: column;
gap: 20px;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 500;
}
.status-badge.enabled {
background: #d4edda;
color: #155724;
}
.status-badge.disabled {
background: #f8d7da;
color: #721c24;
}
#currentProxySettings {
font-family: monospace;
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
font-size: 0.9em;
margin-top: 5px;
}
/* 底部样式 */
.footer {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
text-align: center;
color: #666;
}
.footer a {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 15px;
}
.header-content {
flex-direction: column;
gap: 15px;
text-align: center;
}
.main-content {
grid-template-columns: 1fr;
gap: 20px;
}
.proxy-controls {
flex-direction: column;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.stat-item {
padding: 15px;
}
.stat-value {
font-size: 1.5em;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
}
/* 加载动画 */
.loading {
opacity: 0.6;
pointer-events: none;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`
}
// getJS 返回 JavaScript 代码
func (g *GUIServer) getJS() string {
return `
// GUI 应用主类
class WormholeGUI {
constructor() {
this.isLoading = false;
this.updateInterval = null;
this.init();
}
// 初始化应用
init() {
this.bindEvents();
this.startAutoUpdate();
this.updateStatus();
}
// 绑定事件
bindEvents() {
const toggleBtn = document.getElementById('toggleProxy');
const refreshBtn = document.getElementById('refreshStatus');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => this.toggleProxy());
}
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.updateStatus());
}
// 页面可见性变化时处理
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.stopAutoUpdate();
} else {
this.startAutoUpdate();
this.updateStatus();
}
});
}
// 开始自动更新
startAutoUpdate() {
if (this.updateInterval) return;
this.updateInterval = setInterval(() => {
if (!document.hidden) {
this.updateStatus();
}
}, 5000); // 每5秒更新一次
}
// 停止自动更新
stopAutoUpdate() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
}
// 更新状态
async updateStatus() {
if (this.isLoading) return;
try {
this.setLoading(true);
// 并行获取所有数据
const [status, systemProxy, routingStats] = await Promise.all([
this.fetchAPI('/api/status'),
this.fetchAPI('/api/system/proxy'),
this.fetchAPI('/api/routing/stats')
]);
this.updateStatusDisplay(status);
this.updateProxyStats(status.proxy_stats);
this.updateSystemProxy(systemProxy);
this.updateRoutingStats(routingStats);
} catch (error) {
console.error('更新状态失败:', error);
this.showError('无法连接到服务器');
} finally {
this.setLoading(false);
}
}
// 切换代理状态
async toggleProxy() {
const toggleBtn = document.getElementById('toggleProxy');
const toggleText = document.getElementById('toggleText');
if (!toggleBtn || this.isLoading) return;
try {
this.setLoading(true);
toggleBtn.disabled = true;
// 获取当前状态
const currentStatus = await this.fetchAPI('/api/system/proxy');
const isEnabled = currentStatus.enabled;
// 切换状态
const response = await this.fetchAPI('/api/proxy/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ enable: !isEnabled })
});
if (response.success) {
toggleText.textContent = response.enabled ? '禁用系统代理' : '启用系统代理';
toggleBtn.className = response.enabled ? 'btn btn-secondary' : 'btn btn-primary';
// 立即更新状态
setTimeout(() => this.updateStatus(), 500);
} else {
this.showError(response.error || '操作失败');
}
} catch (error) {
console.error('切换代理失败:', error);
this.showError('操作失败请检查权限');
} finally {
toggleBtn.disabled = false;
this.setLoading(false);
}
}
// 更新状态显示
updateStatusDisplay(status) {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const modeText = document.getElementById('modeText');
if (statusDot && statusText) {
if (status.proxy_stats) {
statusDot.className = 'status-dot connected';
statusText.textContent = '服务运行中';
} else {
statusDot.className = 'status-dot disconnected';
statusText.textContent = '服务未运行';
}
}
if (modeText) {
modeText.textContent = status.mode || 'unknown';
}
}
// 更新代理统计
updateProxyStats(stats) {
if (!stats) return;
this.updateElement('totalConnections', stats.total_connections);
this.updateElement('activeConnections', stats.active_connections);
this.updateElement('successRate', this.formatSuccessRate(stats));
this.updateElement('uptime', this.formatDuration(stats.uptime));
this.updateElement('bytesTransferred', this.formatBytes(stats.bytes_sent + stats.bytes_received));
this.updateElement('failedRequests', stats.failed_requests);
}
// 更新系统代理信息
updateSystemProxy(proxyInfo) {
const statusElement = document.getElementById('systemProxyStatus');
const settingsElement = document.getElementById('currentProxySettings');
const toggleBtn = document.getElementById('toggleProxy');
const toggleText = document.getElementById('toggleText');
if (statusElement) {
if (proxyInfo.enabled) {
statusElement.textContent = '已启用';
statusElement.className = 'status-badge enabled';
} else {
statusElement.textContent = '已禁用';
statusElement.className = 'status-badge disabled';
}
}
if (settingsElement && proxyInfo.current_proxy) {
const settings = Object.entries(proxyInfo.current_proxy)
.map(([key, value]) => key + ': ' + value)
.join('\n');
settingsElement.textContent = settings || '无代理设置';
}
if (toggleBtn && toggleText) {
toggleText.textContent = proxyInfo.enabled ? '禁用系统代理' : '启用系统代理';
toggleBtn.className = proxyInfo.enabled ? 'btn btn-secondary' : 'btn btn-primary';
}
}
// 更新路由统计
updateRoutingStats(stats) {
if (!stats) return;
this.updateElement('bypassDomainsCount', stats.bypass_domains_count);
this.updateElement('forceDomainsCount', stats.force_domains_count);
this.updateElement('bypassLocal', stats.bypass_local ? '是' : '否');
this.updateElement('bypassPrivate', stats.bypass_private ? '是' : '否');
}
// 通用元素更新
updateElement(id, value) {
const element = document.getElementById(id);
if (element) {
element.textContent = value !== undefined ? value : '-';
}
}
// 设置加载状态
setLoading(loading) {
this.isLoading = loading;
const refreshBtn = document.getElementById('refreshStatus');
if (refreshBtn) {
const icon = refreshBtn.querySelector('i');
if (loading) {
icon.classList.add('spin');
refreshBtn.disabled = true;
} else {
icon.classList.remove('spin');
refreshBtn.disabled = false;
}
}
}
// API 请求
async fetchAPI(url, options = {}) {
const response = await fetch(url, {
timeout: 10000,
...options
});
if (!response.ok) {
throw new Error(response.status + ': ' + response.statusText);
}
return await response.json();
}
// 显示错误
showError(message) {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
if (statusDot && statusText) {
statusDot.className = 'status-dot disconnected';
statusText.textContent = message;
}
console.error('GUI Error:', message);
}
// 格式化成功率
formatSuccessRate(stats) {
const total = stats.successful_requests + stats.failed_requests;
if (total === 0) return '0%';
return ((stats.successful_requests / total) * 100).toFixed(1) + '%';
}
// 格式化持续时间
formatDuration(durationNs) {
if (!durationNs) return '0s';
const seconds = Math.floor(durationNs / 1000000000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return days + 'd ' + (hours % 24) + 'h';
if (hours > 0) return hours + 'h ' + (minutes % 60) + 'm';
if (minutes > 0) return minutes + 'm ' + (seconds % 60) + 's';
return seconds + 's';
}
// 格式化字节数
formatBytes(bytes) {
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return size.toFixed(1) + ' ' + units[unitIndex];
}
}
// 页面加载完成后启动应用
document.addEventListener('DOMContentLoaded', () => {
window.wormholeGUI = new WormholeGUI();
});
// 页面卸载时清理
window.addEventListener('beforeunload', () => {
if (window.wormholeGUI) {
window.wormholeGUI.stopAutoUpdate();
}
});
`
}

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

@ -0,0 +1,57 @@
#!/bin/bash
# Wormhole SOCKS5 Client GUI 演示脚本
set -e
echo "🚀 Wormhole SOCKS5 Client GUI 演示"
echo "=================================="
echo
# 检查二进制文件是否存在
if [ ! -f "bin/wormhole-client" ]; then
echo "❌ 未找到 wormhole-client 二进制文件"
echo "💡 请先运行: make build"
exit 1
fi
# 检查配置文件
if [ ! -f "configs/client.yaml" ]; then
echo "❌ 未找到配置文件: configs/client.yaml"
exit 1
fi
echo "✅ 检查通过,开始启动服务..."
echo
# 启动 HTTP 代理模式(用于演示)
echo "🌐 启动 HTTP 代理模式..."
echo "💡 这将启动代理服务和 Web GUI"
echo
echo "📋 服务信息:"
echo " - 代理模式: HTTP"
echo " - 本地端口: 8080"
echo " - 配置文件: configs/client.yaml"
echo
echo "🖥 GUI 访问地址:"
echo " - Web 界面: http://127.0.0.1:8080/gui"
echo " - 统计 API: http://127.0.0.1:8080/stats"
echo " - 健康检查: http://127.0.0.1:8080/health"
echo
echo "📱 浏览器代理设置:"
echo " - HTTP 代理: 127.0.0.1:8080"
echo " - HTTPS 代理: 127.0.0.1:8080"
echo
echo "⚠ 注意: 请确保 SOCKS5 服务器配置正确"
echo "💡 提示: 按 Ctrl+C 停止服务"
echo
echo "🔄 启动中..."
sleep 2
# 启动服务
exec ./bin/wormhole-client -config configs/client.yaml -mode http
Loading…
Cancel
Save