diff --git a/.env.example b/.env.example index 6bfe7e3..9cc10f3 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +2,11 @@ CONTAINER_NAME=goproxy # 端口配置 -STABLE_PORT=7776 # 最低延迟代理端口 -RANDOM_PORT=7777 # 随机轮换代理端口 -WEBUI_PORT=7778 # WebUI 管理端口 +STABLE_PORT=7776 # HTTP 最低延迟代理端口 +RANDOM_PORT=7777 # HTTP 随机轮换代理端口 +WEBUI_PORT=7778 # WebUI 管理端口 +SOCKS5_RANDOM_PORT=7779 # SOCKS5 随机轮换代理端口 +SOCKS5_STABLE_PORT=7780 # SOCKS5 最低延迟代理端口 # 地理过滤配置 # 屏蔽指定国家代码的出口代理(逗号分隔,如 CN,RU,KP) diff --git a/README.md b/README.md index e6e26b7..d9f4ad4 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,23 @@ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Go Version](https://img.shields.io/badge/Go-1.25-00ADD8?logo=go)](https://go.dev/) -GoProxy 从多个公开代理源自动抓取 HTTP/SOCKS5 代理,通过严格验证(出口 IP + 位置 + 延迟)后加入智能代理池,对外提供统一的 HTTP 代理服务。系统采用质量分级、智能补充、自动优化等机制,确保代理池始终保持高质量和稳定性。 +GoProxy 从多个公开代理源自动抓取 HTTP/SOCKS5 代理,通过严格验证(出口 IP + 位置 + 延迟)后加入智能代理池,对外提供 **HTTP 和 SOCKS5 双协议**代理服务。系统采用质量分级、智能补充、自动优化等机制,确保代理池始终保持高质量和稳定性。 **GitHub**:[github.com/isboyjc/GoProxy](https://github.com/isboyjc/GoProxy) ![](https://cdn.isboyjc.com/img/Xnip2026-03-29_03-35-06.png) +## 📋 快速导航 + +- [✨ 核心特性](#-核心特性) - 智能池子、按需抓取、健康管理、双协议支持 +- [🔌 HTTP vs SOCKS5 协议对比](#-http-vs-socks5-协议对比) - 协议区别、使用建议 +- [🚀 快速开始](#-快速开始) - 本地运行、端口说明、代理使用 +- [🐳 Docker 部署](#-docker-部署) - 容器化部署、环境配置、安全建议 +- [⚙️ 配置说明](#️-配置说明) - 完整参数详解、性能优化 +- [🛠️ 开发与调试](#️-开发与调试) - 日志查看、数据库操作 +- [🧪 测试代理服务](#-测试代理服务) - 测试脚本、持续测试、认证测试 +- [❓ 常见问题](#-常见问题) - SOCKS5 使用、浏览器配置、故障排查 + ## ✨ 核心特性 ### 🎯 智能池子机制 @@ -41,12 +52,17 @@ GoProxy 从多个公开代理源自动抓取 HTTP/SOCKS5 代理,通过严格 - **用户无感知**:自动重试在服务端完成,用户只会收到成功响应或最终失败提示 - **防重复尝试**:已尝试过的失败代理不会在同一请求中再次使用 -### 🚪 双端口策略 -- **7777 端口(随机轮换)**:每次请求随机选择代理,IP 高度分散,适合爬虫和数据采集 -- **7776 端口(最低延迟)**:固定使用延迟最低的代理,除非失败才切换,适合长连接和流媒体 -- **自动切换**:两个端口都支持失败自动重试,7776 失败后切换到次优代理 -- **共享池子**:两个端口使用同一个代理池,统一管理和优化 -- **可选认证**:支持 Basic Auth 认证保护,对外开放时可启用 +### 🚪 多端口多协议支持 +- **双协议支持**:同时提供 HTTP 和 SOCKS5 协议服务,满足不同应用需求 +- **双模式策略**:每种协议都提供随机轮换和最低延迟两种模式 +- **4 个服务端口**: + - `7777` - HTTP 随机轮换(IP 多样性,适合爬虫) + - `7776` - HTTP 最低延迟(稳定连接,适合流媒体) + - `7779` - SOCKS5 随机轮换(IP 多样性,适合浏览器/游戏) + - `7780` - SOCKS5 最低延迟(稳定连接,适合固定应用) +- **自动切换**:所有端口都支持失败自动重试,智能切换备用代理 +- **共享池子**:四个端口使用同一个代理池,统一管理和优化 +- **可选认证**:支持 Basic Auth(HTTP)和用户名/密码认证(SOCKS5),对外开放时可启用 ### 🎨 黑客风格 WebUI - **Matrix 美学**:荧光绿 + 纯黑背景,CRT 扫描线效果,JetBrains Mono 等宽字体 @@ -58,15 +74,40 @@ GoProxy 从多个公开代理源自动抓取 HTTP/SOCKS5 代理,通过严格 - **交互优化**:点击地址复制、单个代理刷新、实时日志倒计时 ### 📊 适用场景 +- **Web 开发测试**:HTTP 代理测试 API、爬虫开发、数据采集 +- **浏览器代理**:SOCKS5 协议配置浏览器代理,访问受限网站 +- **命令行工具**:curl、wget、git 等工具使用 HTTP 代理 +- **应用代理**:需要 SOCKS5 协议的应用(SSH、游戏、聊天工具) - **小型 VPS**:低资源消耗(固定池子 + 按需抓取 + 限流查询) - **稳定需求**:自动剔除失败代理,始终保持健康池子 - **质量优先**:S/A 级代理优先使用,自动优化延迟 +### 🔌 HTTP vs SOCKS5 协议对比 + +| 特性 | HTTP 代理 | SOCKS5 代理 | +|------|----------|-------------| +| **协议支持** | 仅 HTTP/HTTPS | 所有 TCP 协议(HTTP/HTTPS/SSH/FTP/游戏等) | +| **工作层级** | 应用层(Layer 7) | 会话层(Layer 5) | +| **浏览器支持** | ✅ 良好 | ✅ 更好(无协议限制) | +| **命令行工具** | ✅ curl/wget 原生支持 | ⚠️ 部分工具需要额外配置 | +| **非 HTTP 应用** | ❌ 不支持 | ✅ 完全支持(SSH/游戏/聊天) | +| **UDP 支持** | ❌ 不支持 | ✅ 支持(SOCKS5 协议特性) | +| **性能开销** | 较低 | 稍高 | +| **认证方式** | Basic Auth | 用户名/密码 | + +**快速选择建议**: +- 🌐 **HTTP 代理**(端口 7776/7777):适合 Web 开发、API 测试、爬虫、数据采集 +- 🔒 **SOCKS5 代理**(端口 7779/7780):适合浏览器、SSH 隧道、游戏、聊天应用、需要完整协议支持的场景 + +**架构设计**: +- HTTP 代理服务:可使用池中的 HTTP 或 SOCKS5 上游代理 +- SOCKS5 代理服务:**仅使用 SOCKS5 上游代理**(因为许多免费 HTTP 代理不支持 HTTPS CONNECT 方法) + ### 📝 扩展文档 - [地理过滤配置指南](GEO_FILTER.md) - 国家代码、使用场景、测试方法 - [数据目录说明](DATA_DIRECTORY.md) - 数据库、配置文件、备份恢复 -- [测试脚本使用](test/README.md) - Bash/Go/Python 测试脚本详细说明 +- [测试脚本使用](test/README.md) - HTTP + SOCKS5 测试脚本详细说明 - [架构设计文档](POOL_DESIGN.md) - 完整的系统设计和实现细节 ## 📦 项目结构 @@ -84,11 +125,14 @@ GoProxy 从多个公开代理源自动抓取 HTTP/SOCKS5 代理,通过严格 ├── checker/ # 🆕 分批健康检查器 ├── optimizer/ # 🆕 优化轮换器(定时优化池子质量) ├── storage/ # 🆕 扩展存储层(质量等级、使用统计、源状态表) -├── proxy/ # 🆕 对外 HTTP 代理服务(双端口 + 可选认证) +├── proxy/ # 🆕 对外代理服务(HTTP + SOCKS5 双协议,4 端口 + 可选认证) +│ ├── server.go # HTTP 代理服务器 +│ └── socks5_server.go # SOCKS5 代理服务器 ├── webui/ # 🆕 黑客风格 WebUI(健康仪表盘、配置界面、RBAC) ├── logger/ # 内存日志收集 ├── test/ # 🧪 测试脚本与文档 -│ ├── test_proxy.sh # Bash 测试脚本 +│ ├── test_proxy.sh # HTTP 代理测试脚本(Bash) +│ ├── test_socks5.sh # SOCKS5 代理测试脚本(Bash) │ ├── test_proxy.go # Go 测试脚本 │ ├── test_proxy.py # Python 测试脚本 │ └── README.md # 测试脚本使用说明 @@ -134,22 +178,34 @@ go build -o proxygo . - 状态监控(每 30 秒) - 健康检查(默认 5 分钟) - 优化轮换(默认 30 分钟) -7. 启动两个代理服务(支持可选认证): - - `:7776` - 最低延迟模式(稳定连接) - - `:7777` - 随机轮换模式(IP 多样性) +7. 启动四个代理服务(支持可选认证): + - `:7776` - HTTP 最低延迟模式(稳定连接) + - `:7777` - HTTP 随机轮换模式(IP 多样性) + - `:7779` - SOCKS5 随机轮换模式(IP 多样性) + - `:7780` - SOCKS5 最低延迟模式(稳定连接) ### 默认端口 -- **代理服务(随机轮换)**:`7777` - 每次请求随机选择代理,IP 多样性高 -- **代理服务(最低延迟)**:`7776` - 固定使用延迟最低的代理,性能优先 + +#### HTTP 代理端口 +- **7777 端口(HTTP 随机轮换)**:每次请求随机选择代理,IP 多样性高 +- **7776 端口(HTTP 最低延迟)**:固定使用延迟最低的代理,性能优先 + +#### SOCKS5 代理端口 +- **7779 端口(SOCKS5 随机轮换)**:每次连接随机选择代理,IP 多样性高 +- **7780 端口(SOCKS5 最低延迟)**:固定使用延迟最低的代理,性能优先 + +#### 管理端口 - **WebUI**:`7778` - **默认密码**:`goproxy`(可通过 `WEBUI_PASSWORD` 环境变量自定义) - **访问方式**:本地使用 `localhost`,远程使用服务器 IP 地址 ### 使用代理 -GoProxy 提供**两个代理端口**,满足不同场景需求: +GoProxy 提供**四个代理端口**,支持 HTTP 和 SOCKS5 两种协议,满足不同场景需求: -#### 🎲 7777 端口 - 随机轮换模式 +#### 🌐 HTTP 协议代理 + +##### 🎲 7777 端口 - HTTP 随机轮换模式 适合需要 **IP 多样性** 的场景(爬虫、数据采集、负载均衡): @@ -166,7 +222,7 @@ curl -x http://your-server-ip:7777 https://httpbin.org/ip - 优先使用高质量(S/A 级)代理 - IP 地址高度分散 -#### ⚡ 7776 端口 - 最低延迟模式 +##### ⚡ 7776 端口 - HTTP 最低延迟模式 适合需要 **稳定连接** 的场景(长连接、流媒体、实时通信): @@ -184,9 +240,49 @@ curl -x http://your-server-ip:7776 https://httpbin.org/ip - 失败时自动删除并切换到下一个最低延迟代理 - 性能和稳定性最优 +#### 🔌 SOCKS5 协议代理 + +##### 🎲 7779 端口 - SOCKS5 随机轮换模式 + +适合需要 **原生 SOCKS5** 和 **IP 多样性** 的场景: + +```bash +# 使用 curl(需要 7.21.7+) +curl --socks5 localhost:7779 https://httpbin.org/ip + +# 远程使用 +curl --socks5 your-server-ip:7779 https://httpbin.org/ip + +# 使用 proxychains +echo "socks5 127.0.0.1 7779" > ~/.proxychains.conf +proxychains4 curl https://httpbin.org/ip +``` + +**特点**: +- 原生 SOCKS5 协议,更广泛的应用支持 +- 每次连接随机选择代理 +- 支持 TCP 和 UDP(如果上游代理支持) + +##### ⚡ 7780 端口 - SOCKS5 最低延迟模式 + +适合需要 **SOCKS5 协议** 和 **稳定连接** 的场景: + +```bash +# 本地使用 +curl --socks5 localhost:7780 https://httpbin.org/ip + +# 远程使用 +curl --socks5 your-server-ip:7780 https://httpbin.org/ip +``` + +**特点**: +- 固定使用延迟最低的代理 +- 适合需要 SOCKS5 协议的应用(如浏览器、游戏客户端) +- 最佳性能和稳定性 + #### 环境变量配置 -**本地使用**: +**HTTP 代理**: ```bash # 使用随机模式 export http_proxy=http://localhost:7777 @@ -195,25 +291,77 @@ export https_proxy=http://localhost:7777 # 或使用稳定模式 export http_proxy=http://localhost:7776 export https_proxy=http://localhost:7776 + +# 远程使用(带认证) +export http_proxy=http://proxy:your_password@your-server-ip:7777 +export https_proxy=http://proxy:your_password@your-server-ip:7777 ``` -**远程使用(带认证)**: +**SOCKS5 代理**(更多应用支持): ```bash -# 格式:http://username:password@host:port -export http_proxy=http://proxy:your_password@192.168.1.100:7777 -export https_proxy=http://proxy:your_password@192.168.1.100:7777 +# 使用随机模式 +export ALL_PROXY=socks5://localhost:7779 + +# 或使用稳定模式 +export ALL_PROXY=socks5://localhost:7780 + +# 远程使用(带认证) +export ALL_PROXY=socks5://proxy:your_password@your-server-ip:7779 ``` #### 端口对比 -| 特性 | 7777(随机轮换) | 7776(最低延迟) | -|------|-----------------|-----------------| -| **选择策略** | 随机选择(优先高质量) | 固定使用延迟最低的 | -| **IP 多样性** | ⭐⭐⭐⭐⭐ 高度分散 | ⭐ 基本固定 | -| **连接稳定性** | ⭐⭐⭐ 每次切换 | ⭐⭐⭐⭐⭐ 固定不变 | -| **性能表现** | ⭐⭐⭐⭐ 平均延迟 | ⭐⭐⭐⭐⭐ 最优延迟 | -| **适用场景** | 爬虫、数据采集、防封禁 | 长连接、流媒体、下载 | -| **失败切换** | 自动重试 3 次 | 失败后切换到次优代理 | +| 端口 | 协议 | 模式 | IP 多样性 | 稳定性 | 性能 | 适用场景 | +|------|------|------|----------|--------|------|---------| +| **7777** | HTTP | 随机轮换 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | 爬虫、数据采集 | +| **7776** | HTTP | 最低延迟 | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 长连接、流媒体 | +| **7779** | SOCKS5 | 随机轮换 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | 浏览器、游戏、SSH | +| **7780** | SOCKS5 | 最低延迟 | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 稳定应用连接 | + +#### 协议选择建议 + +**何时使用 HTTP 代理(7776/7777)**: +- 简单的 HTTP/HTTPS 请求 +- curl、wget 等命令行工具 +- 大多数编程语言的 HTTP 客户端 + +**何时使用 SOCKS5 代理(7779/7780)**: +- 需要代理非 HTTP 协议(如 SSH、FTP、游戏) +- 浏览器代理设置(SOCKS5 支持更好) +- 需要 UDP 支持的应用 +- 某些应用只支持 SOCKS5 协议 + +#### SOCKS5 使用示例 + +**Python**: +```python +import requests + +proxies = { + 'http': 'socks5://localhost:7779', + 'https': 'socks5://localhost:7779' +} +response = requests.get('https://httpbin.org/ip', proxies=proxies) +``` + +**Node.js**: +```javascript +const SocksProxyAgent = require('socks-proxy-agent'); +const fetch = require('node-fetch'); + +const agent = new SocksProxyAgent('socks5://localhost:7779'); +fetch('https://httpbin.org/ip', { agent }).then(res => res.json()); +``` + +**浏览器配置**: +- 类型:SOCKS5 +- 地址:`localhost` 或服务器 IP +- 端口:`7779`(随机)或 `7780`(稳定) + +**SSH 隧道**: +```bash +ssh -o ProxyCommand='nc -X 5 -x localhost:7779 %h %p' user@remote-server +``` #### 自动重试机制说明 @@ -283,6 +431,8 @@ docker run -d --name proxygo \ -p 7776:7776 \ -p 7777:7777 \ -p 7778:7778 \ + -p 7779:7779 \ + -p 7780:7780 \ -e WEBUI_PASSWORD=your_password \ -v goproxy-data:/app/data \ ghcr.io/isboyjc/goproxy:latest @@ -327,16 +477,18 @@ docker compose up -d | `PROXY_AUTH_USERNAME` | `proxy` | 代理认证用户名 | | `PROXY_AUTH_PASSWORD` | 空 | 代理认证密码 | | `WEBUI_PASSWORD` | `goproxy` | WebUI 登录密码 | -| `STABLE_PORT` | `7776` | 最低延迟代理端口 | -| `RANDOM_PORT` | `7777` | 随机轮换代理端口 | +| `STABLE_PORT` | `7776` | HTTP 最低延迟代理端口 | +| `RANDOM_PORT` | `7777` | HTTP 随机轮换代理端口 | | `WEBUI_PORT` | `7778` | WebUI 管理端口 | +| `SOCKS5_STABLE_PORT` | `7780` | SOCKS5 最低延迟代理端口 | +| `SOCKS5_RANDOM_PORT` | `7779` | SOCKS5 随机轮换代理端口 | 完整环境变量列表请查看 `.env.example` 文件。 **⚠️ 生产部署注意事项**: - 如使用 Dokploy、Coolify 等平台部署,确保 `docker-compose.yml` 中配置了平台网络(如 `dokploy-network`) - WebUI 端口可通过平台的域名功能访问,无需手动配置端口绑定 -- 代理端口(7776/7777)通过 `IP:端口` 直接访问,**强烈建议启用认证** +- 代理端口(7776/7777/7779/7780)通过 `IP:端口` 直接访问,**强烈建议启用认证** **常用配置示例**: @@ -371,7 +523,7 @@ docker compose up -d ### 安全部署配置 -**默认配置**:代理服务对外开放(端口 7776、7777),WebUI 对外开放(端口 7778)。 +**默认配置**:代理服务对外开放(端口 7776、7777、7779、7780),WebUI 对外开放(端口 7778)。 **⚠️ 重要提示**: @@ -393,15 +545,21 @@ WEBUI_PASSWORD=admin_pass **客户端使用(带认证)**: ```bash -# 环境变量方式 +# HTTP 代理 - 环境变量方式 export http_proxy=http://myuser:secure_pass_123@server-ip:7777 export https_proxy=http://myuser:secure_pass_123@server-ip:7777 -# curl 直接指定 +# HTTP 代理 - curl 直接指定 curl -x http://myuser:secure_pass_123@server-ip:7777 https://httpbin.org/ip -# Python requests -proxies = {'http': 'http://myuser:secure_pass_123@server-ip:7777', 'https': '...'} +# SOCKS5 代理 - curl 使用 +curl --socks5 myuser:secure_pass_123@server-ip:7779 https://httpbin.org/ip + +# Python - HTTP 代理 +proxies = {'http': 'http://myuser:secure_pass_123@server-ip:7777', 'https': 'http://myuser:secure_pass_123@server-ip:7777'} + +# Python - SOCKS5 代理 +proxies = {'http': 'socks5://myuser:secure_pass_123@server-ip:7779', 'https': 'socks5://myuser:secure_pass_123@server-ip:7779'} ``` ## ⚙️ 配置说明 @@ -433,8 +591,10 @@ proxies = {'http': 'http://myuser:secure_pass_123@server-ip:7777', 'https': '... | 参数 | 默认值 | 说明 | | --- | --- | --- | -| `proxy_port` | `:7777` | 随机轮换代理端口 | -| `stable_proxy_port` | `:7776` | 最低延迟代理端口 | +| `proxy_port` | `:7777` | HTTP 随机轮换代理端口 | +| `stable_proxy_port` | `:7776` | HTTP 最低延迟代理端口 | +| `socks5_port` | `:7779` | SOCKS5 随机轮换代理端口 | +| `stable_socks5_port` | `:7780` | SOCKS5 最低延迟代理端口 | | `webui_port` | `:7778` | WebUI 端口 | **代理认证配置** @@ -443,9 +603,12 @@ proxies = {'http': 'http://myuser:secure_pass_123@server-ip:7777', 'https': '... | --- | --- | --- | | `proxy_auth_enabled` | `false` | 是否启用代理认证(对外开放时建议启用) | | `proxy_auth_username` | `proxy` | 代理认证用户名 | -| `proxy_auth_password_hash` | 空 | 代理认证密码 SHA256 哈希(通过环境变量 `PROXY_AUTH_PASSWORD` 设置原始密码) | +| `proxy_auth_password_hash` | 空 | 代理认证密码 SHA256 哈希(HTTP Basic Auth) | -> 💡 **注意**:代理认证配置通过**环境变量**设置,不在 `config.json` 中。启动时从 `PROXY_AUTH_ENABLED`、`PROXY_AUTH_USERNAME`、`PROXY_AUTH_PASSWORD` 环境变量读取。 +> 💡 **注意**: +> - 代理认证配置通过**环境变量**设置,不在 `config.json` 中 +> - 启动时从 `PROXY_AUTH_ENABLED`、`PROXY_AUTH_USERNAME`、`PROXY_AUTH_PASSWORD` 环境变量读取 +> - **HTTP 代理**使用 Basic Auth(密码哈希),**SOCKS5 代理**使用用户名/密码认证(明文传输,建议内网使用) **池子容量配置** @@ -916,13 +1079,16 @@ A: PROXY_AUTH_PASSWORD=mypass ``` 2. 重启服务:`docker compose up -d` -3. 客户端使用:`http://myuser:mypass@server-ip:7777` +3. 客户端使用: + - HTTP:`http://myuser:mypass@server-ip:7777` + - SOCKS5:`socks5://myuser:mypass@server-ip:7779` ### Q: 代理认证和 WebUI 认证有什么区别? A: -- **代理认证**:保护 7776/7777 代理服务端口,防止代理被滥用 +- **代理认证**:保护代理服务端口(7776/7777/7779/7780),防止代理被滥用 - **WebUI 认证**:保护 7778 管理后台,区分访客和管理员权限 - 两者独立配置,互不影响 +- 启用代理认证时,HTTP 和 SOCKS5 代理都需要认证 ## 🛠️ 开发与调试 @@ -937,6 +1103,7 @@ A: - `[health]`:健康检查器 - `[optimize]`:优化器 - `[monitor]`:状态监控器 +- `[socks5]`:SOCKS5 代理服务器(握手、认证、连接建立) ### 数据库操作 @@ -956,35 +1123,52 @@ sqlite3 data/proxy.db "DELETE FROM proxies;" ## 🧪 测试代理服务 -项目提供了三种测试脚本,用于验证代理服务功能和性能(位于 `test/` 目录)。 +项目提供了多种测试脚本,用于验证 HTTP 和 SOCKS5 代理服务功能和性能(位于 `test/` 目录)。 ### 快速测试 +**HTTP 代理测试**: ```bash -# 测试随机轮换模式(默认 7777 端口) +# 测试 HTTP 随机轮换模式(7777 端口) ./test/test_proxy.sh -# 测试最低延迟模式(7776 端口) +# 测试 HTTP 最低延迟模式(7776 端口) ./test/test_proxy.sh 7776 -# 使用 Go 脚本 -go run test/test_proxy.go # 默认 7777 -go run test/test_proxy.go 7776 # 测试 7776 +# 使用 Go/Python 脚本 +go run test/test_proxy.go 7777 +python test/test_proxy.py 7776 +``` -# 使用 Python 脚本 -python test/test_proxy.py # 默认 7777 -python test/test_proxy.py 7776 # 测试 7776 +**SOCKS5 代理测试**: +```bash +# 测试 SOCKS5 随机轮换模式(7779 端口) +./test/test_socks5.sh localhost 7779 + +# 测试 SOCKS5 最低延迟模式(7780 端口) +./test/test_socks5.sh localhost 7780 + +# 持续测试 50 次 +./test/test_socks5.sh localhost 7779 50 # 按 Ctrl+C 停止测试并查看统计 ``` -测试脚本特点: +**HTTP 测试脚本**(`test_proxy.sh`)特点: - **持续运行模式**:类似 `ping` 命令,持续发送请求 -- 实时显示每次请求的出口 IP 和延迟 +- 实时显示每次请求的出口 IP、国家和延迟 - 动态更新成功率统计 -- 验证代理轮换机制 +- 验证 HTTP 代理轮换机制 - 按 `Ctrl+C` 停止并显示完整统计报告 +**SOCKS5 测试脚本**(`test_socks5.sh`)特点: +- 使用 `curl --socks5-hostname -k` 测试 SOCKS5 协议 +- 显示出口 IP、国家、延迟和时间戳 +- 验证 SOCKS5 代理轮换和稳定性 +- 支持指定测试次数或持续测试 +- 实时统计成功率和平均延迟 +- 自动跳过 SSL 证书验证(免费代理常有证书问题) + ### 测试输出示例 ``` @@ -1004,15 +1188,153 @@ proxy from 🇯🇵 198.51.100.12: seq=5 time=890ms **测试认证功能**: ```bash -# 启用认证后测试 +# HTTP 代理 - 带认证 curl -x http://myuser:mypass@localhost:7777 https://httpbin.org/ip -# 无认证请求(应该返回 407 错误) +# HTTP 代理 - 无认证(应该返回 407 错误) curl -x http://localhost:7777 https://httpbin.org/ip + +# SOCKS5 代理 - 带认证 +curl --socks5 myuser:mypass@localhost:7779 https://httpbin.org/ip + +# SOCKS5 代理 - 无认证(应该连接失败) +curl --socks5 localhost:7779 https://httpbin.org/ip ``` **测试脚本使用**:[`test/README.md`](./test/README.md) +## ❓ 常见问题 + +### Q1: HTTP 和 SOCKS5 代理有什么区别? + +**简单理解**: +- **HTTP 代理**:专门为 HTTP/HTTPS 设计,简单易用,命令行工具支持好 +- **SOCKS5 代理**:通用代理协议,支持所有 TCP/UDP 协议,功能更强大 + +**详细对比**见上方 [HTTP vs SOCKS5 协议对比](#-http-vs-socks5-协议对比) 章节。 + +### Q2: 我应该使用哪个端口? + +根据你的需求: + +| 需求 | 推荐端口 | 理由 | +|------|---------|------| +| 🕷️ **爬虫/数据采集** | 7777(HTTP 随机) | IP 高度分散,降低封禁风险 | +| 🎥 **流媒体/长连接** | 7776(HTTP 稳定) | 延迟最低,连接稳定 | +| 🌐 **浏览器代理** | 7779(SOCKS5 随机) | 协议支持好,IP 多样性高 | +| 🎮 **游戏/SSH/聊天** | 7780(SOCKS5 稳定) | 非 HTTP 协议,稳定优先 | + +### Q3: SOCKS5 代理支持认证吗? + +支持。当 `PROXY_AUTH_ENABLED=true` 时,所有代理端口(包括 SOCKS5)都需要认证: + +```bash +# SOCKS5 带认证 +curl --socks5 username:password@server-ip:7779 https://httpbin.org/ip + +# 浏览器配置 +# SOCKS5 Host: server-ip +# SOCKS5 Port: 7779 +# Username: username +# Password: password +``` + +### Q4: 为什么推荐"域名:端口"访问而非域名直接配置代理? + +**技术限制**: +- Cloudflare 等 DNS 托管服务的 HTTP 代理(橙云)**不支持非标准端口**(80/443 以外) +- 即使关闭橙云,DNS 记录也无法直接指向容器内部端口 + +**正确做法**: +```bash +# ✅ 推荐方式:IP:端口 或 域名:端口 +curl -x http://proxy.example.com:7777 https://httpbin.org/ip +curl -x http://123.45.67.89:7777 https://httpbin.org/ip + +# ❌ 不支持:直接域名(除非反向代理到 80/443) +curl -x http://proxy.example.com https://httpbin.org/ip +``` + +### Q5: 如何测试 SOCKS5 代理是否正常工作? + +使用提供的测试脚本: + +```bash +# 测试 SOCKS5 随机端口(7779) +./test/test_socks5.sh localhost 7779 + +# 持续测试 50 次 +./test/test_socks5.sh localhost 7779 50 +``` + +或手动测试: +```bash +# curl 测试(-k 跳过 SSL 证书验证) +curl -k --socks5-hostname localhost:7779 https://httpbin.org/ip + +# 或测试 HTTP(不需要 SSL) +curl --socks5-hostname localhost:7779 http://httpbin.org/ip + +# Python 测试(需要安装 requests[socks]) +python3 -c "import requests; print(requests.get('https://httpbin.org/ip', proxies={'http': 'socks5://localhost:7779', 'https': 'socks5://localhost:7779'}).json())" +``` + +### Q6: SOCKS5 代理失败率是否比 HTTP 高? + +不会。两种服务质量和成功率相近,但**使用不同的上游代理**: +- **HTTP 代理服务**(7776/7777):可使用池中的 HTTP 或 SOCKS5 上游代理 +- **SOCKS5 代理服务**(7779/7780):仅使用 SOCKS5 上游代理(因为许多免费 HTTP 代理不支持 HTTPS CONNECT) +- 两种服务都支持自动重试和故障切换 + +### Q7: 如何在浏览器中配置 SOCKS5 代理? + +**Chrome/Edge**(使用插件如 SwitchyOmega): +1. 安装 Proxy SwitchyOmega 插件 +2. 新建情景模式 → SOCKS5 +3. 代理服务器:`server-ip` 或 `localhost` +4. 代理端口:`7779`(随机)或 `7780`(稳定) +5. 如启用认证,填写用户名和密码 + +**Firefox**: +1. 设置 → 网络设置 → 手动代理配置 +2. SOCKS Host:`server-ip` 或 `localhost` +3. Port:`7779` 或 `7780` +4. 选择:SOCKS v5 +5. 勾选"通过 SOCKS 代理 DNS 查询"(可选) + +### Q8: 如何查看 SOCKS5 服务的运行日志? + +**本地运行**: +```bash +# 直接查看终端输出,SOCKS5 日志前缀为 [socks5] +# 启动日志示例: +# socks5 server listening on :7779 [随机轮换] [需认证 (用户: proxy)] +# socks5 server listening on :7780 [最低延迟] [无认证] + +# 连接日志示例: +# [socks5] google.com:443 via 203.0.113.45:1080 established +``` + +**Docker 部署**: +```bash +# 查看最近 50 行日志 +docker logs proxygo --tail 50 + +# 实时跟踪日志(含 SOCKS5) +docker logs proxygo -f + +# 过滤 SOCKS5 相关日志 +docker logs proxygo -f | grep socks5 + +# 查看 SOCKS5 错误日志 +docker logs proxygo --tail 200 | grep -i "socks5.*failed" +``` + +**WebUI 查看**: +- 访问 WebUI(端口 7778)→ 点击右上角 **Logs** 按钮 +- 实时日志面板会显示所有协议的连接和错误信息 +- 包括 SOCKS5 握手、认证、连接建立等详细日志 + ## 🙏 致谢与声明 本项目基于 [jonasen1988/proxygo](https://github.com/jonasen1988/proxygo) 进行魔改和增强。 @@ -1029,12 +1351,13 @@ curl -x http://localhost:7777 https://httpbin.org/ip - 🆕 **按需抓取策略**:源分组、断路器保护、Emergency/Refill/Optimize 多模式 - 🆕 **分层健康管理**:批次检查、智能跳过 S 级、定时优化轮换 - 🆕 **智能重试机制**:自动故障切换、失败即删除、防重复尝试 -- 🆕 **双端口服务**:7777 随机轮换(IP 多样性)+ 7776 最低延迟(稳定连接) -- 🆕 **代理认证保护**:可选 Basic Auth 认证,对外开放时保护代理服务不被滥用 +- 🆕 **双协议支持**:HTTP + SOCKS5 双协议,4 个服务端口(随机/稳定各 2 个) +- 🆕 **双模式策略**:每种协议都支持随机轮换(IP 多样性)和最低延迟(稳定连接) +- 🆕 **代理认证保护**:HTTP Basic Auth + SOCKS5 用户名/密码认证,对外开放时保护服务 - 🆕 **黑客风格 WebUI**:Matrix 美学、实时仪表盘、完整配置界面、中英文切换 - 🆕 **双角色权限**:访客模式(只读)+ 管理员模式(完全控制),可安全公网开放 - 🆕 **扩展存储层**:质量等级、使用统计、源状态管理 -- 🆕 **测试套件**:Bash/Go/Python 三种测试脚本,持续运行模式,显示国旗 emoji +- 🆕 **测试套件**:HTTP + SOCKS5 测试脚本,持续运行模式,显示国旗 emoji - 🆕 **CI/CD 自动化**:GitHub Actions 自动构建多架构镜像(amd64/arm64),双仓库发布 - 🆕 **环境变量配置**:docker-compose + .env 文件,灵活配置各种部署场景 diff --git a/config/config.go b/config/config.go index 97604b7..8e639ce 100644 --- a/config/config.go +++ b/config/config.go @@ -34,10 +34,17 @@ type Config struct { // 稳定代理端口(最低延迟模式) StableProxyPort string + // SOCKS5 服务端口(随机轮换模式) + SOCKS5Port string + + // 稳定 SOCKS5 端口(最低延迟模式) + StableSOCKS5Port string + // 代理服务认证配置 ProxyAuthEnabled bool // 是否启用代理认证(默认 false) ProxyAuthUsername string // 代理认证用户名(默认 "proxy") - ProxyAuthPasswordHash string // 代理认证密码 SHA256 哈希 + ProxyAuthPassword string // 代理认证密码明文(用于 SOCKS5) + ProxyAuthPasswordHash string // 代理认证密码 SHA256 哈希(用于 HTTP) // 地理过滤配置 BlockedCountries []string // 屏蔽的国家代码列表(如 ["CN", "RU"],默认 ["CN"]) @@ -139,11 +146,14 @@ func DefaultConfig() *Config { WebUIPasswordHash: passwordHash(password), ProxyPort: ":7777", StableProxyPort: ":7776", + SOCKS5Port: ":7779", + StableSOCKS5Port: ":7780", DBPath: dataDir() + "proxy.db", // 代理认证配置 ProxyAuthEnabled: proxyAuthEnabled, ProxyAuthUsername: proxyAuthUsername, + ProxyAuthPassword: proxyAuthPassword, ProxyAuthPasswordHash: proxyAuthHash, // 地理过滤配置 diff --git a/docker-compose.yml b/docker-compose.yml index 36e6f47..b3163db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,9 +10,11 @@ services: container_name: ${CONTAINER_NAME:-goproxy} restart: unless-stopped ports: - - "${STABLE_PORT:-7776}:7776" # 稳定代理(最低延迟) - - "${RANDOM_PORT:-7777}:7777" # 随机轮换 - - "${WEBUI_PORT:-7778}:7778" # WebUI(有登录认证) + - "${STABLE_PORT:-7776}:7776" # HTTP 最低延迟 + - "${RANDOM_PORT:-7777}:7777" # HTTP 随机轮换 + - "${WEBUI_PORT:-7778}:7778" # WebUI(有登录认证) + - "${SOCKS5_RANDOM_PORT:-7779}:7779" # SOCKS5 随机轮换 + - "${SOCKS5_STABLE_PORT:-7780}:7780" # SOCKS5 最低延迟 volumes: - goproxy-data:/app/data environment: diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index d9928b9..74f101d 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -71,13 +71,13 @@ func (f *Fetcher) FetchSmart(mode string, preferredProtocol string) ([]storage.P switch mode { case "emergency": - // 紧急模式:使用所有可用源 - sources = f.filterAvailableSources(allSources, preferredProtocol) - log.Printf("[fetch] 🚨 紧急模式: 使用 %d 个源", len(sources)) + // 紧急模式:忽略断路器,强制使用所有源(包括被禁用的) + sources = f.filterAvailableSources(allSources, preferredProtocol, true) + log.Printf("[fetch] 🚨 紧急模式: 使用 %d 个源(忽略断路器)", len(sources)) case "refill": // 补充模式:使用快更新源 - sources = f.filterAvailableSources(fastUpdateSources, preferredProtocol) + sources = f.filterAvailableSources(fastUpdateSources, preferredProtocol, false) log.Printf("[fetch] 🔄 补充模式: 使用 %d 个快更新源", len(sources)) case "optimize": @@ -86,7 +86,7 @@ func (f *Fetcher) FetchSmart(mode string, preferredProtocol string) ([]storage.P log.Printf("[fetch] ⚡ 优化模式: 使用 %d 个源", len(sources)) default: - sources = f.filterAvailableSources(fastUpdateSources, preferredProtocol) + sources = f.filterAvailableSources(fastUpdateSources, preferredProtocol, false) } if len(sources) == 0 { @@ -97,11 +97,12 @@ func (f *Fetcher) FetchSmart(mode string, preferredProtocol string) ([]storage.P } // filterAvailableSources 过滤可用的源(通过断路器) -func (f *Fetcher) filterAvailableSources(sources []Source, preferredProtocol string) []Source { +// ignoreCircuitBreaker: 是否忽略断路器(Emergency 模式下使用) +func (f *Fetcher) filterAvailableSources(sources []Source, preferredProtocol string, ignoreCircuitBreaker bool) []Source { var available []Source for _, src := range sources { - // 检查断路器 - if f.sourceManager != nil && !f.sourceManager.CanUseSource(src.URL) { + // 检查断路器(紧急模式下忽略) + if !ignoreCircuitBreaker && f.sourceManager != nil && !f.sourceManager.CanUseSource(src.URL) { continue } // 如果指定了协议偏好,优先该协议的源 @@ -115,7 +116,7 @@ func (f *Fetcher) filterAvailableSources(sources []Source, preferredProtocol str // selectRandomSources 随机选择N个源 func (f *Fetcher) selectRandomSources(sources []Source, count int, preferredProtocol string) []Source { - available := f.filterAvailableSources(sources, preferredProtocol) + available := f.filterAvailableSources(sources, preferredProtocol, false) if len(available) <= count { return available } diff --git a/main.go b/main.go index 1f60dc7..18a1c78 100644 --- a/main.go +++ b/main.go @@ -70,9 +70,13 @@ func main() { totalDeleted += int(deleted) } - // 创建两个代理服务器:随机轮换 + 最低延迟 + // 创建 HTTP 代理服务器:随机轮换 + 最低延迟 randomServer := proxy.New(store, cfg, "random", cfg.ProxyPort) stableServer := proxy.New(store, cfg, "lowest-latency", cfg.StableProxyPort) + + // 创建 SOCKS5 代理服务器:随机轮换 + 最低延迟 + socks5RandomServer := proxy.NewSOCKS5(store, cfg, "random", cfg.SOCKS5Port) + socks5StableServer := proxy.NewSOCKS5(store, cfg, "lowest-latency", cfg.StableSOCKS5Port) // 配置变更通知 channel configChanged := make(chan struct{}, 1) @@ -105,16 +109,30 @@ func main() { // 监听配置变更 go watchConfigChanges(configChanged, poolMgr) - // 启动稳定代理服务(最低延迟模式) + // 启动 HTTP 稳定代理服务(最低延迟模式) go func() { if err := stableServer.Start(); err != nil { - log.Fatalf("stable proxy server: %v", err) + log.Fatalf("stable http proxy server: %v", err) } }() - // 启动随机代理服务(阻塞) + // 启动 SOCKS5 稳定代理服务(最低延迟模式) + go func() { + if err := socks5StableServer.Start(); err != nil { + log.Fatalf("stable socks5 proxy server: %v", err) + } + }() + + // 启动 SOCKS5 随机代理服务 + go func() { + if err := socks5RandomServer.Start(); err != nil { + log.Fatalf("random socks5 proxy server: %v", err) + } + }() + + // 启动 HTTP 随机代理服务(阻塞) if err := randomServer.Start(); err != nil { - log.Fatalf("random proxy server: %v", err) + log.Fatalf("random http proxy server: %v", err) } } diff --git a/proxy/socks5_server.go b/proxy/socks5_server.go new file mode 100644 index 0000000..3dc74e8 --- /dev/null +++ b/proxy/socks5_server.go @@ -0,0 +1,440 @@ +package proxy + +import ( + "encoding/binary" + "fmt" + "io" + "log" + "net" + "time" + + "goproxy/config" + "goproxy/storage" +) + +// SOCKS5Server SOCKS5 协议服务器 +type SOCKS5Server struct { + storage *storage.Storage + cfg *config.Config + mode string // "random" 或 "lowest-latency" + port string +} + +// NewSOCKS5 创建 SOCKS5 服务器 +func NewSOCKS5(s *storage.Storage, cfg *config.Config, mode string, port string) *SOCKS5Server { + return &SOCKS5Server{ + storage: s, + cfg: cfg, + mode: mode, + port: port, + } +} + +// Start 启动 SOCKS5 服务器 +func (s *SOCKS5Server) Start() error { + modeDesc := "随机轮换" + if s.mode == "lowest-latency" { + modeDesc = "最低延迟" + } + authStatus := "无认证" + if s.cfg.ProxyAuthEnabled { + authStatus = fmt.Sprintf("需认证 (用户: %s)", s.cfg.ProxyAuthUsername) + } + log.Printf("socks5 server listening on %s [%s] [%s]", s.port, modeDesc, authStatus) + + listener, err := net.Listen("tcp", s.port) + if err != nil { + return err + } + defer listener.Close() + + for { + conn, err := listener.Accept() + if err != nil { + continue + } + go s.handleConnection(conn) + } +} + +// handleConnection 处理 SOCKS5 连接 +func (s *SOCKS5Server) handleConnection(clientConn net.Conn) { + defer clientConn.Close() + + // SOCKS5 握手 + if err := s.socks5Handshake(clientConn); err != nil { + log.Printf("[socks5] handshake failed: %v", err) + return + } + + // 读取请求 + target, err := s.readSOCKS5Request(clientConn) + if err != nil { + log.Printf("[socks5] read request failed: %v", err) + return + } + + // 带重试的连接上游代理 + // 重试机制:只使用 SOCKS5 协议的上游代理(天然支持 HTTPS) + tried := []string{} + maxRetries := s.cfg.MaxRetry + 2 // 增加重试次数以应对质量差的代理 + + for attempt := 0; attempt <= maxRetries; attempt++ { + var p *storage.Proxy + var err error + + // SOCKS5 服务只使用 SOCKS5 上游代理 + if s.mode == "lowest-latency" { + p, err = s.storage.GetLowestLatencyByProtocolExclude("socks5", tried) + } else { + p, err = s.storage.GetRandomByProtocolExclude("socks5", tried) + } + + if err != nil { + log.Printf("[socks5] no available socks5 upstream proxy: %v", err) + s.sendSOCKS5Reply(clientConn, 0x01) // General failure + return + } + + tried = append(tried, p.Address) + + // 连接上游代理 + upstreamConn, err := s.dialViaProxy(p, target) + if err != nil { + log.Printf("[socks5] dial %s via %s (%s) failed: %v, removing", target, p.Address, p.Protocol, err) + s.storage.Delete(p.Address) + continue + } + + // 发送成功响应 + if err := s.sendSOCKS5Reply(clientConn, 0x00); err != nil { + upstreamConn.Close() + return + } + + log.Printf("[socks5] %s via %s established", target, p.Address) + + // 双向转发数据 + go io.Copy(upstreamConn, clientConn) + io.Copy(clientConn, upstreamConn) + + // 转发完成,关闭连接 + upstreamConn.Close() + return + } + + // 所有重试都失败 + s.sendSOCKS5Reply(clientConn, 0x01) // General failure + log.Printf("[socks5] all proxies failed for %s", target) +} + +// socks5Handshake 处理 SOCKS5 握手 +func (s *SOCKS5Server) socks5Handshake(conn net.Conn) error { + buf := make([]byte, 257) + + // 读取客户端问候: [VER(1), NMETHODS(1), METHODS(1-255)] + n, err := io.ReadAtLeast(conn, buf, 2) + if err != nil { + return err + } + + version := buf[0] + if version != 0x05 { + return fmt.Errorf("unsupported SOCKS version: %d", version) + } + + nmethods := int(buf[1]) + if n < 2+nmethods { + if _, err := io.ReadFull(conn, buf[n:2+nmethods]); err != nil { + return err + } + } + + // 检查是否需要认证 + needAuth := s.cfg.ProxyAuthEnabled + methods := buf[2 : 2+nmethods] + + // 选择认证方式 + var selectedMethod byte = 0xFF // No acceptable methods + if needAuth { + // 需要用户名/密码认证 (0x02) + for _, method := range methods { + if method == 0x02 { + selectedMethod = 0x02 + break + } + } + } else { + // 无需认证 (0x00) + for _, method := range methods { + if method == 0x00 { + selectedMethod = 0x00 + break + } + } + } + + // 发送方法选择: [VER(1), METHOD(1)] + if _, err := conn.Write([]byte{0x05, selectedMethod}); err != nil { + return err + } + + if selectedMethod == 0xFF { + return fmt.Errorf("no acceptable authentication method") + } + + // 如果需要认证,进行用户名/密码认证 + if selectedMethod == 0x02 { + if err := s.socks5Auth(conn); err != nil { + return err + } + } + + return nil +} + +// socks5Auth 处理 SOCKS5 用户名/密码认证 +func (s *SOCKS5Server) socks5Auth(conn net.Conn) error { + buf := make([]byte, 513) + + // 读取认证请求: [VER(1), ULEN(1), UNAME(1-255), PLEN(1), PASSWD(1-255)] + n, err := io.ReadAtLeast(conn, buf, 2) + if err != nil { + return err + } + + if buf[0] != 0x01 { + return fmt.Errorf("unsupported auth version: %d", buf[0]) + } + + ulen := int(buf[1]) + if n < 2+ulen { + if _, err := io.ReadFull(conn, buf[n:2+ulen]); err != nil { + return err + } + n = 2 + ulen + } + + username := string(buf[2 : 2+ulen]) + + // 读取密码长度和密码 + if n < 2+ulen+1 { + if _, err := io.ReadFull(conn, buf[n:2+ulen+1]); err != nil { + return err + } + n = 2 + ulen + 1 + } + + plen := int(buf[2+ulen]) + if n < 2+ulen+1+plen { + if _, err := io.ReadFull(conn, buf[n:2+ulen+1+plen]); err != nil { + return err + } + } + + password := string(buf[2+ulen+1 : 2+ulen+1+plen]) + + // 验证用户名和密码 + if username != s.cfg.ProxyAuthUsername || password != s.cfg.ProxyAuthPassword { + // 认证失败: [VER(1), STATUS(1)] + conn.Write([]byte{0x01, 0x01}) + return fmt.Errorf("authentication failed") + } + + // 认证成功: [VER(1), STATUS(1)] + if _, err := conn.Write([]byte{0x01, 0x00}); err != nil { + return err + } + + return nil +} + +// readSOCKS5Request 读取 SOCKS5 请求 +func (s *SOCKS5Server) readSOCKS5Request(conn net.Conn) (string, error) { + buf := make([]byte, 262) + + // 读取请求: [VER(1), CMD(1), RSV(1), ATYP(1), DST.ADDR(variable), DST.PORT(2)] + n, err := io.ReadAtLeast(conn, buf, 4) + if err != nil { + return "", err + } + + if buf[0] != 0x05 { + return "", fmt.Errorf("invalid version: %d", buf[0]) + } + + cmd := buf[1] + if cmd != 0x01 { // 只支持 CONNECT + s.sendSOCKS5Reply(conn, 0x07) // Command not supported + return "", fmt.Errorf("unsupported command: %d", cmd) + } + + atyp := buf[3] + var host string + var addrLen int + + switch atyp { + case 0x01: // IPv4 + addrLen = 4 + if n < 4+addrLen+2 { + if _, err := io.ReadFull(conn, buf[n:4+addrLen+2]); err != nil { + return "", err + } + } + host = fmt.Sprintf("%d.%d.%d.%d", buf[4], buf[5], buf[6], buf[7]) + case 0x03: // Domain name + addrLen = int(buf[4]) + if n < 4+1+addrLen+2 { + if _, err := io.ReadFull(conn, buf[n:4+1+addrLen+2]); err != nil { + return "", err + } + } + host = string(buf[5 : 5+addrLen]) + case 0x04: // IPv6 + addrLen = 16 + if n < 4+addrLen+2 { + if _, err := io.ReadFull(conn, buf[n:4+addrLen+2]); err != nil { + return "", err + } + } + // 简化处理,直接转换 + host = net.IP(buf[4 : 4+addrLen]).String() + default: + s.sendSOCKS5Reply(conn, 0x08) // Address type not supported + return "", fmt.Errorf("unsupported address type: %d", atyp) + } + + // 读取端口 + portOffset := 4 + if atyp == 0x03 { + portOffset = 5 + addrLen + } else { + portOffset = 4 + addrLen + } + port := binary.BigEndian.Uint16(buf[portOffset : portOffset+2]) + + return fmt.Sprintf("%s:%d", host, port), nil +} + +// sendSOCKS5Reply 发送 SOCKS5 响应 +func (s *SOCKS5Server) sendSOCKS5Reply(conn net.Conn, rep byte) error { + // [VER(1), REP(1), RSV(1), ATYP(1), BND.ADDR(variable), BND.PORT(2)] + // 简化:使用 0.0.0.0:0 + reply := []byte{ + 0x05, // VER + rep, // REP: 0x00=成功, 0x01=一般失败, 0x07=命令不支持, 0x08=地址类型不支持 + 0x00, // RSV + 0x01, // ATYP: IPv4 + 0, 0, 0, 0, // BND.ADDR: 0.0.0.0 + 0, 0, // BND.PORT: 0 + } + _, err := conn.Write(reply) + return err +} + +// dialViaProxy 通过上游代理连接目标 +func (s *SOCKS5Server) dialViaProxy(p *storage.Proxy, target string) (net.Conn, error) { + timeout := time.Duration(s.cfg.ValidateTimeout) * time.Second + + switch p.Protocol { + case "http": + // 连接到 HTTP 代理 + conn, err := net.DialTimeout("tcp", p.Address, timeout) + if err != nil { + return nil, err + } + // 发送 CONNECT 请求 + fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", target, target) + buf := make([]byte, 256) + n, err := conn.Read(buf) + if err != nil { + conn.Close() + return nil, err + } + // 检查 HTTP 响应 + if n < 12 || string(buf[:12]) != "HTTP/1.1 200" { + conn.Close() + return nil, fmt.Errorf("upstream proxy connect failed") + } + return conn, nil + + case "socks5": + // 使用 SOCKS5 代理 + dialer := &net.Dialer{Timeout: timeout} + proxyConn, err := dialer.Dial("tcp", p.Address) + if err != nil { + return nil, err + } + + // SOCKS5 握手(无认证) + if _, err := proxyConn.Write([]byte{0x05, 0x01, 0x00}); err != nil { + proxyConn.Close() + return nil, err + } + + handshake := make([]byte, 2) + if _, err := io.ReadFull(proxyConn, handshake); err != nil { + proxyConn.Close() + return nil, err + } + + if handshake[0] != 0x05 || handshake[1] != 0x00 { + proxyConn.Close() + return nil, fmt.Errorf("socks5 handshake failed") + } + + // 发送 CONNECT 请求 + host, port, err := net.SplitHostPort(target) + if err != nil { + proxyConn.Close() + return nil, err + } + + // 构建请求 + req := []byte{0x05, 0x01, 0x00} // VER, CMD=CONNECT, RSV + + // 判断是 IP 还是域名 + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + req = append(req, 0x01) // IPv4 + req = append(req, ip4...) + } else { + req = append(req, 0x04) // IPv6 + req = append(req, ip...) + } + } else { + req = append(req, 0x03) // Domain + req = append(req, byte(len(host))) + req = append(req, []byte(host)...) + } + + // 添加端口 + portNum := uint16(0) + fmt.Sscanf(port, "%d", &portNum) + portBytes := make([]byte, 2) + binary.BigEndian.PutUint16(portBytes, portNum) + req = append(req, portBytes...) + + if _, err := proxyConn.Write(req); err != nil { + proxyConn.Close() + return nil, err + } + + // 读取响应 + reply := make([]byte, 10) + if _, err := io.ReadAtLeast(proxyConn, reply, 10); err != nil { + proxyConn.Close() + return nil, err + } + + if reply[1] != 0x00 { + proxyConn.Close() + return nil, fmt.Errorf("socks5 connect failed, code: %d", reply[1]) + } + + return proxyConn, nil + + default: + return nil, fmt.Errorf("unsupported protocol: %s", p.Protocol) + } +} diff --git a/storage/storage.go b/storage/storage.go index 6817ad4..6a4e8e9 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -177,11 +177,21 @@ func (s *Storage) initSchema() error { // AddProxy 新增代理,已存在则忽略 func (s *Storage) AddProxy(address, protocol string) error { - _, err := s.db.Exec( + result, err := s.db.Exec( `INSERT OR IGNORE INTO proxies (address, protocol) VALUES (?, ?)`, address, protocol, ) - return err + if err != nil { + log.Printf("[storage] AddProxy %s error: %v", address, err) + return err + } + + // 检查是否真的插入了 + affected, _ := result.RowsAffected() + if affected == 0 { + log.Printf("[storage] AddProxy %s ignored (already exists or constraint)", address) + } + return nil } // AddProxies 批量新增 @@ -328,6 +338,56 @@ func (s *Storage) GetLowestLatencyExclude(excludes []string) (*Proxy, error) { return nil, fmt.Errorf("no available proxy") } +// GetRandomByProtocolExclude 按协议获取随机代理(排除已尝试的) +func (s *Storage) GetRandomByProtocolExclude(protocol string, excludes []string) (*Proxy, error) { + proxies, err := s.GetAll() + if err != nil { + return nil, err + } + + excludeMap := make(map[string]bool) + for _, e := range excludes { + excludeMap[e] = true + } + + var available []Proxy + for _, p := range proxies { + if p.Protocol == protocol && !excludeMap[p.Address] { + available = append(available, p) + } + } + + if len(available) == 0 { + return nil, fmt.Errorf("no %s proxy available", protocol) + } + + proxy := available[time.Now().UnixNano()%int64(len(available))] + return &proxy, nil +} + +// GetLowestLatencyByProtocolExclude 按协议获取最低延迟代理(排除已尝试的) +func (s *Storage) GetLowestLatencyByProtocolExclude(protocol string, excludes []string) (*Proxy, error) { + proxies, err := s.GetAll() + if err != nil { + return nil, err + } + + excludeMap := make(map[string]bool) + for _, e := range excludes { + excludeMap[e] = true + } + + // GetAll() 已经按 latency ASC 排序,找到第一个匹配协议且不在排除列表中的 + for _, p := range proxies { + if p.Protocol == protocol && !excludeMap[p.Address] { + proxy := p + return &proxy, nil + } + } + + return nil, fmt.Errorf("no %s proxy available", protocol) +} + // Delete 立即删除指定代理 func (s *Storage) Delete(address string) error { _, err := s.db.Exec(`DELETE FROM proxies WHERE address = ?`, address) diff --git a/test/README.md b/test/README.md index 8db1a35..a7585d1 100644 --- a/test/README.md +++ b/test/README.md @@ -7,6 +7,7 @@ | 脚本 | 语言 | 依赖 | 运行模式 | 推荐度 | |------|------|------|----------|--------| | `test_proxy.sh` | Bash | curl + Python3 | 持续运行 | ⭐⭐⭐ | +| `test_socks5.sh` | Bash | curl + Python3 | 持续运行 | ⭐⭐⭐ | | `test_proxy.go` | Go | `golang.org/x/net/proxy` | 持续运行 | ⭐⭐ | | `test_proxy.py` | Python | `requests`, `pysocks` | 持续运行 | ⭐⭐ | @@ -14,15 +15,24 @@ ### Bash 脚本(推荐) +**HTTP 代理测试**: ```bash -# 从项目根目录运行(持续测试 HTTP) -./test/test_proxy.sh +# 测试 7777 端口(随机轮换) +./test/test_proxy.sh 7777 -# 测试 HTTP 协议 -./test/test_proxy.sh http +# 测试 7776 端口(最低延迟) +./test/test_proxy.sh 7776 -# 测试 SOCKS5 协议 -./test/test_proxy.sh socks5 +# 按 Ctrl+C 停止并查看统计 +``` + +**SOCKS5 代理测试**: +```bash +# 测试 7779 端口(随机轮换) +./test/test_socks5.sh 7779 + +# 测试 7780 端口(最低延迟) +./test/test_socks5.sh 7780 # 按 Ctrl+C 停止并查看统计 ``` @@ -69,9 +79,9 @@ python test/test_proxy.py ## 🔀 测试不同端口策略 -```bash -# 对比两个端口的行为差异: +### HTTP 代理端口对比 +```bash # 随机轮换模式 - IP 高度分散 ./test/test_proxy.sh 7777 @@ -83,6 +93,22 @@ python test/test_proxy.py - **7777 端口**:每次请求的出口 IP 应该不同(证明在轮换) - **7776 端口**:连续多次请求的出口 IP 基本相同(证明固定使用最优代理) +### SOCKS5 代理端口对比 + +```bash +# 随机轮换模式 - IP 高度分散 +./test/test_socks5.sh 7779 + +# 最低延迟模式 - 固定使用最快代理 +./test/test_socks5.sh 7780 +``` + +**观察要点**: +- **7779 端口**:每次连接的出口 IP 应该不同 +- **7780 端口**:连续多次连接的出口 IP 基本相同 + +> 💡 **提示**:SOCKS5 测试脚本使用 `-k` 参数跳过 SSL 证书验证,因为免费上游代理常有证书问题。生产环境建议使用质量更好的付费代理。 + ## 🔍 预期输出 ``` diff --git a/test/test_proxy.sh b/test/test_proxy.sh index 66d6bb8..fec327c 100755 --- a/test/test_proxy.sh +++ b/test/test_proxy.sh @@ -4,6 +4,8 @@ # 按 Ctrl+C 停止测试 # 用法: ./test_proxy.sh [端口号,默认7777] +# PROXY_HOST="192.227.184.201" +# PROXY_HOST="proxy.amux.ai" PROXY_HOST="127.0.0.1" PROXY_PORT="${1:-7777}" TEST_URL="http://ip-api.com/json/?fields=countryCode,query" diff --git a/test/test_socks5.sh b/test/test_socks5.sh new file mode 100755 index 0000000..68a6622 --- /dev/null +++ b/test/test_socks5.sh @@ -0,0 +1,75 @@ +#!/bin/bash +### + # @LastEditTime: 2026-03-29 23:26:29 + # @Description: ... + # @Date: 2026-03-29 23:14:32 + # @Author: isboyjc + # @LastEditors: isboyjc +### + +# GoProxy SOCKS5 代理测试脚本 +# 用法: ./test_socks5.sh [端口号,默认7779] + +PROXY_HOST="${PROXY_HOST:-127.0.0.1}" +PROXY_PORT="${1:-7779}" +TEST_URL="https://httpbin.org/ip" +DELAY=1 + +# 统计变量 +total=0 +success=0 +fail=0 + +# 获取毫秒时间戳(兼容 macOS 和 Linux) +get_ms_time() { + python3 -c 'import time; print(int(time.time() * 1000))' +} + +# 国家代码转 emoji 旗帜 +country_to_emoji() { + local country_code="$1" + if [ -z "$country_code" ] || [ "$country_code" = "null" ]; then + echo "🌐" + return + fi + + local first="${country_code:0:1}" + local second="${country_code:1:1}" + python3 -c "print(chr(127462 + ord('$first') - ord('A')) + chr(127462 + ord('$second') - ord('A')))" +} + +# 捕获 Ctrl+C 信号 +trap ctrl_c INT +function ctrl_c() { + echo "" + echo "---" + loss_rate=$(awk "BEGIN {printf \"%.1f\", ($total - $success)/$total*100}") + echo "$total requests transmitted, $success received, $((total - success)) failed, ${loss_rate}% packet loss" + exit 0 +} + +echo "SOCKS5 PROXY ${PROXY_HOST}:${PROXY_PORT}: continuous mode" +echo "" + +while true; do + total=$((total + 1)) + + # 使用 curl 的 SOCKS5 支持(-k 跳过 SSL 验证,因为免费代理证书常有问题) + start=$(get_ms_time) + response=$(curl -s -k --socks5-hostname ${PROXY_HOST}:${PROXY_PORT} ${TEST_URL} --max-time 10 2>&1) + end=$(get_ms_time) + latency=$((end - start)) + + if echo "$response" | grep -q '"origin"'; then + success=$((success + 1)) + origin=$(echo "$response" | grep -o '"origin":"[^"]*"' | cut -d'"' -f4 | cut -d',' -f1) + country=$(curl -s "http://ip-api.com/json/${origin}?fields=countryCode" 2>/dev/null | grep -o '"countryCode":"[^"]*"' | cut -d'"' -f4) + emoji=$(country_to_emoji "$country") + echo "socks5 #${total}: ${origin} ${emoji} ${country} (${latency}ms)" + else + fail=$((fail + 1)) + echo "socks5 #${total}: request failed" + fi + + sleep $DELAY +done