From 63db36ce01da937285aaebf6bbd7a9b975cbf581 Mon Sep 17 00:00:00 2001 From: pqcqaq <905739777@qq.com> Date: Sat, 18 Oct 2025 20:25:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BA=86=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=B3=A8=E5=86=8C=EF=BC=88windows=E4=B8=8Elinux?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QUICK_START_SERVICE.md | 403 ++++++++++++++++++ README.md | 77 +++- SERVICE_GUIDE.md | 494 ++++++++++++++++++++++ src/client/main.go | 208 +++++++-- src/common/service/service.go | 62 +++ src/common/service/service_linux.go | 220 ++++++++++ src/common/service/service_unsupported.go | 44 ++ src/common/service/service_windows.go | 275 ++++++++++++ src/common/service/wrapper_windows.go | 77 ++++ src/go.mod | 6 +- src/go.sum | 2 + src/server/main.go | 246 ++++++++--- 12 files changed, 2002 insertions(+), 112 deletions(-) create mode 100644 QUICK_START_SERVICE.md create mode 100644 SERVICE_GUIDE.md create mode 100644 src/common/service/service.go create mode 100644 src/common/service/service_linux.go create mode 100644 src/common/service/service_unsupported.go create mode 100644 src/common/service/service_windows.go create mode 100644 src/common/service/wrapper_windows.go diff --git a/QUICK_START_SERVICE.md b/QUICK_START_SERVICE.md new file mode 100644 index 0000000..96d614a --- /dev/null +++ b/QUICK_START_SERVICE.md @@ -0,0 +1,403 @@ +# 服务注册快速开始 + +## 快速测试(非管理员权限) + +### 1. 正常运行模式(不注册服务) + +```powershell +# Server +cd bin +.\server.exe -config config.yaml + +# Client +.\client.exe -server localhost:9000 +``` + +## 注册为 Windows 服务(需要管理员权限) + +### Server 服务 + +#### 步骤 1: 以管理员身份打开 PowerShell + +右键点击 PowerShell → "以管理员身份运行" + +#### 步骤 2: 进入程序目录 + +```powershell +cd D:\Develop\Projects\go-tunnel\bin +``` + +#### 步骤 3: 安装服务 + +```powershell +.\server.exe -reg install +``` + +输出示例: +``` +服务 Go Tunnel Server 安装成功 +可执行文件: D:\Develop\Projects\go-tunnel\bin\server.exe +启动参数: +工作目录: D:\Develop\Projects\go-tunnel\bin + +使用以下命令管理服务: + 启动服务: sc start GoTunnelServer + 停止服务: sc stop GoTunnelServer + 查询状态: sc query GoTunnelServer + 卸载服务: D:\Develop\Projects\go-tunnel\bin\server.exe -reg uninstall +``` + +#### 步骤 4: 启动服务 + +```powershell +sc start GoTunnelServer +``` + +或者使用: +```powershell +.\server.exe -reg start +``` + +#### 步骤 5: 检查服务状态 + +```powershell +sc query GoTunnelServer +``` + +输出示例: +``` +SERVICE_NAME: GoTunnelServer + TYPE : 10 WIN32_OWN_PROCESS + STATE : 4 RUNNING + ... +``` + +#### 步骤 6: 查看服务日志 + +打开"事件查看器": +1. 按 `Win + R`,输入 `eventvwr.msc` +2. 展开 "Windows 日志" → "应用程序" +3. 在右侧点击"筛选当前日志" +4. 在事件源中搜索相关日志 + +或者查看 API 接口: +```powershell +# 浏览器访问 +http://localhost:8080 +``` + +### Client 服务 + +#### 安装并启动 + +```powershell +# 以管理员身份运行 +cd D:\Develop\Projects\go-tunnel\bin + +# 安装服务 +.\client.exe -server your-server.com:9000 -reg install + +# 启动服务 +sc start GoTunnelClient + +# 查看状态 +sc query GoTunnelClient +``` + +## 管理已安装的服务 + +### 启动服务 + +```powershell +# Server +sc start GoTunnelServer + +# Client +sc start GoTunnelClient +``` + +### 停止服务 + +```powershell +# Server +sc stop GoTunnelServer + +# Client +sc stop GoTunnelClient +``` + +### 重启服务 + +```powershell +# Server +sc stop GoTunnelServer +sc start GoTunnelServer + +# Client +sc stop GoTunnelClient +sc start GoTunnelClient +``` + +### 查看服务状态 + +```powershell +# Server +sc query GoTunnelServer + +# Client +sc query GoTunnelClient +``` + +### 卸载服务 + +```powershell +# Server +.\server.exe -reg uninstall + +# Client +.\client.exe -reg uninstall +``` + +## 使用服务管理器(图形界面) + +### 打开服务管理器 + +1. 按 `Win + R` +2. 输入 `services.msc` +3. 按回车 + +### 在服务列表中找到服务 + +- **Go Tunnel Server** (GoTunnelServer) +- **Go Tunnel Client** (GoTunnelClient) + +### 可用操作 + +- 右键点击服务 +- 选择:启动、停止、重新启动、属性 + +### 查看和修改恢复选项 + +1. 右键点击服务 → "属性" +2. 切换到"恢复"选项卡 +3. 可以看到和修改失败后的恢复策略: + - 第一次失败:重新启动服务(1分钟后) + - 第二次失败:重新启动服务(1分钟后) + - 后续失败:重新启动服务(1分钟后) + +## 故障排查 + +### 问题 1: 服务无法启动 + +#### 检查权限 + +```powershell +# 确认是否以管理员运行 +[Security.Principal.WindowsPrincipal] $user = [Security.Principal.WindowsIdentity]::GetCurrent() +$user.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +# 应该返回 True +``` + +#### 手动测试运行 + +```powershell +# 以普通模式运行,查看错误信息 +.\server.exe -config config.yaml +``` + +#### 检查端口占用 + +```powershell +# 检查 API 端口(默认 8080) +netstat -ano | findstr :8080 + +# 检查隧道端口(默认 9000) +netstat -ano | findstr :9000 +``` + +### 问题 2: 服务启动但无法访问 + +#### 检查防火墙 + +```powershell +# 添加防火墙规则 +New-NetFirewallRule -DisplayName "Go Tunnel Server" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow +New-NetFirewallRule -DisplayName "Go Tunnel Server Tunnel" -Direction Inbound -Protocol TCP -LocalPort 9000 -Action Allow +``` + +#### 测试 API 是否可访问 + +```powershell +# 使用 curl 测试 +curl http://localhost:8080 + +# 或使用浏览器访问 +start http://localhost:8080 +``` + +### 问题 3: 配置文件找不到 + +服务会在工作目录查找配置文件。确保: + +1. `config.yaml` 在可执行文件同一目录 +2. 或者安装时指定配置文件路径: + +```powershell +.\server.exe -config "C:\full\path\to\config.yaml" -reg install +``` + +### 问题 4: 数据库权限问题 + +确保服务有权限访问数据库文件: + +```powershell +# 检查数据库文件权限 +Get-Acl data\tunnel.db | Format-List + +# 如果需要,添加 SYSTEM 账户的权限 +$acl = Get-Acl data\tunnel.db +$permission = "NT AUTHORITY\SYSTEM","FullControl","Allow" +$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission +$acl.SetAccessRule($accessRule) +Set-Acl data\tunnel.db $acl +``` + +## 测试自动重启 + +### 模拟服务崩溃 + +1. 找到服务进程 ID: + +```powershell +Get-Process -Name server | Select-Object Id,ProcessName +``` + +2. 强制结束进程: + +```powershell +Stop-Process -Id <进程ID> -Force +``` + +3. 等待约 60 秒 + +4. 检查服务状态: + +```powershell +sc query GoTunnelServer +``` + +服务应该会自动重启,状态显示为 RUNNING。 + +## 完整示例脚本 + +### 安装和启动 Server 服务 + +```powershell +# install_server.ps1 +# 需要以管理员身份运行 + +$ErrorActionPreference = "Stop" + +Write-Host "检查管理员权限..." +$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) +if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Error "请以管理员身份运行此脚本" + exit 1 +} + +Write-Host "进入程序目录..." +cd $PSScriptRoot + +Write-Host "停止并卸载旧服务(如果存在)..." +sc stop GoTunnelServer 2>$null +.\server.exe -reg uninstall 2>$null + +Write-Host "安装新服务..." +.\server.exe -reg install + +Write-Host "启动服务..." +sc start GoTunnelServer + +Write-Host "等待服务启动..." +Start-Sleep -Seconds 3 + +Write-Host "检查服务状态..." +sc query GoTunnelServer + +Write-Host "`n服务安装完成!" +Write-Host "API 地址: http://localhost:8080" +Write-Host "查看日志: eventvwr.msc" +``` + +### 卸载服务 + +```powershell +# uninstall_server.ps1 +# 需要以管理员身份运行 + +$ErrorActionPreference = "Stop" + +Write-Host "停止服务..." +sc stop GoTunnelServer + +Write-Host "等待服务停止..." +Start-Sleep -Seconds 2 + +Write-Host "卸载服务..." +.\server.exe -reg uninstall + +Write-Host "服务已卸载" +``` + +## 监控脚本 + +### 检查服务运行状态 + +```powershell +# monitor_service.ps1 + +$serviceName = "GoTunnelServer" +$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + +if ($null -eq $service) { + Write-Host "❌ 服务未安装: $serviceName" + exit 1 +} + +if ($service.Status -eq 'Running') { + Write-Host "✅ 服务运行正常: $serviceName" + + # 测试 API 访问 + try { + $response = Invoke-WebRequest -Uri "http://localhost:8080" -TimeoutSec 5 + Write-Host "✅ API 访问正常" + } catch { + Write-Host "⚠️ API 访问失败: $_" + } +} else { + Write-Host "❌ 服务未运行: $serviceName (状态: $($service.Status))" + + # 尝试启动服务 + Write-Host "尝试启动服务..." + Start-Service -Name $serviceName + Start-Sleep -Seconds 3 + + $service.Refresh() + if ($service.Status -eq 'Running') { + Write-Host "✅ 服务启动成功" + } else { + Write-Host "❌ 服务启动失败" + exit 1 + } +} +``` + +可以将监控脚本添加到任务计划程序,定期执行检查。 + +--- + +## 下一步 + +- 📖 查看完整文档:[SERVICE_GUIDE.md](SERVICE_GUIDE.md) +- 🔧 配置文件说明:[README.md](README.md) +- 🐛 问题反馈:提交 Issue diff --git a/README.md b/README.md index f94082d..d39b0e0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ - ✅ **连接池管理**: 高效的连接池和并发管理 - ✅ **优雅关闭**: 支持优雅关闭和信号处理 - ✅ **自动重连**: 客户端支持断线自动重连 +- ✅ **系统服务**: 支持注册为 Windows 服务和 Linux systemd 服务 +- ✅ **自动重启**: 服务崩溃后自动重启,保证高可用性 - ✅ **生产级代码**: 完善的错误处理、日志记录和性能优化 ## 系统架构 @@ -583,7 +585,80 @@ cd bin ./client -help ``` -### 6. 运行测试 +### 6. 注册为系统服务(生产环境推荐) + +#### Windows 服务 + +```powershell +# 以管理员身份运行 PowerShell + +# 安装 Server 服务 +.\server.exe -reg install + +# 安装 Client 服务 +.\client.exe -server your-server.com:9000 -reg install + +# 启动服务 +sc start GoTunnelServer +sc start GoTunnelClient + +# 查看服务状态 +sc query GoTunnelServer +sc query GoTunnelClient + +# 停止服务 +sc stop GoTunnelServer +sc stop GoTunnelClient + +# 卸载服务 +.\server.exe -reg uninstall +.\client.exe -reg uninstall +``` + +#### Linux 服务(systemd) + +```bash +# 以 root 身份运行 + +# 安装 Server 服务 +sudo ./server -reg install + +# 安装 Client 服务 +sudo ./client -server your-server.com:9000 -reg install + +# 启动服务 +sudo systemctl start GoTunnelServer +sudo systemctl start GoTunnelClient + +# 查看服务状态 +sudo systemctl status GoTunnelServer +sudo systemctl status GoTunnelClient + +# 查看日志 +sudo journalctl -u GoTunnelServer -f +sudo journalctl -u GoTunnelClient -f + +# 停止服务 +sudo systemctl stop GoTunnelServer +sudo systemctl stop GoTunnelClient + +# 卸载服务 +sudo ./server -reg uninstall +sudo ./client -reg uninstall +``` + +#### 服务特性 + +- ✅ **开机自启**: 系统启动时自动运行 +- ✅ **自动重启**: 服务崩溃后自动重启(Windows: 60秒后,Linux: 10秒后) +- ✅ **优雅关闭**: 正确处理停止信号,清理资源 +- ✅ **日志管理**: Windows 使用事件查看器,Linux 使用 journalctl + +📖 **详细文档**: +- [服务注册完整指南](SERVICE_GUIDE.md) +- [快速开始指南](QUICK_START_SERVICE.md) + +### 7. 运行测试 ```bash # 运行所有测试 diff --git a/SERVICE_GUIDE.md b/SERVICE_GUIDE.md new file mode 100644 index 0000000..39e13a1 --- /dev/null +++ b/SERVICE_GUIDE.md @@ -0,0 +1,494 @@ +# 服务注册和管理指南 + +## 概述 + +Go Tunnel 支持将 Server 和 Client 注册为系统服务,实现开机自启和自动重启功能。 + +- **Windows**: 使用 Windows Service +- **Linux**: 使用 systemd + +## 功能特性 + +✅ **开机自启**: 系统启动时自动运行 +✅ **自动重启**: 服务崩溃后自动重启(Windows: 60秒后重启,Linux: 10秒后重启) +✅ **优雅关闭**: 正确处理停止信号,清理资源 +✅ **日志记录**: Windows 使用事件查看器,Linux 使用 journalctl +✅ **管理员权限**: 自动检查权限,确保安全安装 + +## 安装要求 + +### Windows +- 管理员权限(以管理员身份运行 PowerShell 或 CMD) +- Windows 7 或更高版本 + +### Linux +- root 权限(使用 sudo) +- systemd 支持(大多数现代 Linux 发行版) + +--- + +## Server 服务管理 + +### Windows + +#### 1. 安装服务 + +```powershell +# 以管理员身份运行 +.\server.exe -reg install + +# 指定配置文件 +.\server.exe -config custom_config.yaml -reg install +``` + +#### 2. 启动服务 + +```powershell +# 方法 1: 使用程序命令 +.\server.exe -reg start + +# 方法 2: 使用 sc 命令 +sc start GoTunnelServer + +# 方法 3: 使用服务管理器(services.msc) +``` + +#### 3. 停止服务 + +```powershell +.\server.exe -reg stop +# 或 +sc stop GoTunnelServer +``` + +#### 4. 查询服务状态 + +```powershell +.\server.exe -reg status +# 或 +sc query GoTunnelServer +``` + +#### 5. 卸载服务 + +```powershell +.\server.exe -reg uninstall +``` + +#### 6. 查看日志 + +打开"事件查看器" → "Windows 日志" → "应用程序",搜索 "GoTunnelServer" + +### Linux + +#### 1. 安装服务 + +```bash +# 以 root 身份运行 +sudo ./server -reg install + +# 指定配置文件 +sudo ./server -config /etc/gotunnel/config.yaml -reg install +``` + +#### 2. 启动服务 + +```bash +# 方法 1: 使用程序命令 +sudo ./server -reg start + +# 方法 2: 使用 systemctl +sudo systemctl start GoTunnelServer +``` + +#### 3. 停止服务 + +```bash +sudo ./server -reg stop +# 或 +sudo systemctl stop GoTunnelServer +``` + +#### 4. 查询服务状态 + +```bash +sudo ./server -reg status +# 或 +sudo systemctl status GoTunnelServer +``` + +#### 5. 卸载服务 + +```bash +sudo ./server -reg uninstall +``` + +#### 6. 查看日志 + +```bash +# 实时查看日志 +sudo journalctl -u GoTunnelServer -f + +# 查看最近的日志 +sudo journalctl -u GoTunnelServer -n 100 + +# 查看今天的日志 +sudo journalctl -u GoTunnelServer --since today +``` + +#### 7. 设置开机自启(安装时自动启用) + +```bash +sudo systemctl enable GoTunnelServer +``` + +--- + +## Client 服务管理 + +### Windows + +#### 1. 安装服务 + +```powershell +# 以管理员身份运行 +.\client.exe -reg install + +# 指定服务器地址 +.\client.exe -server example.com:9000 -reg install +``` + +#### 2. 启动服务 + +```powershell +.\client.exe -reg start +# 或 +sc start GoTunnelClient +``` + +#### 3. 停止服务 + +```powershell +.\client.exe -reg stop +# 或 +sc stop GoTunnelClient +``` + +#### 4. 查询服务状态 + +```powershell +.\client.exe -reg status +# 或 +sc query GoTunnelClient +``` + +#### 5. 卸载服务 + +```powershell +.\client.exe -reg uninstall +``` + +### Linux + +#### 1. 安装服务 + +```bash +sudo ./client -reg install + +# 指定服务器地址 +sudo ./client -server example.com:9000 -reg install +``` + +#### 2. 启动服务 + +```bash +sudo ./client -reg start +# 或 +sudo systemctl start GoTunnelClient +``` + +#### 3. 停止服务 + +```bash +sudo ./client -reg stop +# 或 +sudo systemctl stop GoTunnelClient +``` + +#### 4. 查询服务状态 + +```bash +sudo ./client -reg status +# 或 +sudo systemctl status GoTunnelClient +``` + +#### 5. 卸载服务 + +```bash +sudo ./client -reg uninstall +``` + +#### 6. 查看日志 + +```bash +# 实时查看日志 +sudo journalctl -u GoTunnelClient -f + +# 查看最近的日志 +sudo journalctl -u GoTunnelClient -n 100 +``` + +--- + +## 自动重启配置 + +### Windows + +服务失败时的恢复策略: +- **第一次失败**: 60秒后重启 +- **第二次失败**: 60秒后重启 +- **后续失败**: 60秒后重启 +- **重置失败计数**: 24小时 + +可以通过服务管理器(services.msc)查看和修改恢复选项。 + +### Linux + +systemd 服务配置(自动生成): +```ini +[Service] +Restart=always # 总是重启 +RestartSec=10 # 10秒后重启 +StartLimitBurst=5 # 60秒内最多重启5次 +StartLimitInterval=60 # 限制间隔 +``` + +修改重启策略: +```bash +# 编辑服务文件 +sudo systemctl edit --full GoTunnelServer + +# 修改后重新加载 +sudo systemctl daemon-reload +sudo systemctl restart GoTunnelServer +``` + +--- + +## 故障排查 + +### Windows + +#### 服务无法启动 + +1. 检查事件查看器中的错误日志 +2. 确认可执行文件路径正确 +3. 验证配置文件存在且格式正确 +4. 检查端口是否被占用 + +```powershell +# 查看详细错误 +sc query GoTunnelServer +sc qc GoTunnelServer + +# 手动测试运行 +.\server.exe -config config.yaml +``` + +#### 权限问题 + +确保以管理员身份运行: +```powershell +# 检查是否以管理员运行 +[Security.Principal.WindowsPrincipal] $CurrentUser = [Security.Principal.WindowsIdentity]::GetCurrent() +$CurrentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +``` + +### Linux + +#### 服务无法启动 + +```bash +# 查看详细状态 +sudo systemctl status GoTunnelServer -l + +# 查看启动失败日志 +sudo journalctl -u GoTunnelServer -xe + +# 手动测试运行 +sudo ./server -config config.yaml +``` + +#### 权限问题 + +```bash +# 检查文件权限 +ls -l /etc/systemd/system/GoTunnelServer.service +ls -l ./server + +# 确保可执行 +sudo chmod +x ./server +``` + +#### 端口占用 + +```bash +# 检查端口使用情况 +sudo netstat -tlnp | grep :8080 +sudo lsof -i :8080 +``` + +--- + +## 生产环境最佳实践 + +### 1. 日志管理 + +#### Windows +- 定期清理事件日志 +- 考虑使用第三方日志聚合工具 + +#### Linux +```bash +# 限制日志大小(编辑 journald 配置) +sudo vim /etc/systemd/journald.conf + +# 设置日志保留 +SystemMaxUse=1G +SystemMaxFileSize=100M +MaxRetentionSec=30day + +# 重启 journald +sudo systemctl restart systemd-journald +``` + +### 2. 监控和告警 + +#### Windows +使用 PowerShell 脚本监控服务状态: +```powershell +$service = Get-Service GoTunnelServer +if ($service.Status -ne 'Running') { + # 发送告警 + Write-Host "Service is not running!" +} +``` + +#### Linux +使用 systemd 或第三方监控工具: +```bash +# 检查服务状态 +systemctl is-active GoTunnelServer + +# 配合监控工具(如 Prometheus + Node Exporter) +``` + +### 3. 配置备份 + +定期备份配置文件和数据库: +```bash +# Linux +sudo cp /path/to/config.yaml /backup/config.yaml.$(date +%Y%m%d) + +# Windows +Copy-Item config.yaml backup\config.yaml.$(Get-Date -Format 'yyyyMMdd') +``` + +### 4. 安全建议 + +- ✅ 使用非 root/Administrator 用户运行(可选配置) +- ✅ 限制配置文件权限(仅管理员可读写) +- ✅ 启用防火墙规则 +- ✅ 定期更新程序版本 +- ✅ 监控异常连接和流量 + +### 5. 资源限制 + +#### Linux (systemd) +编辑服务文件添加资源限制: +```ini +[Service] +# 内存限制 +MemoryLimit=512M +# 文件描述符限制 +LimitNOFILE=65536 +# CPU 使用率限制 +CPUQuota=200% +``` + +--- + +## 常见命令速查表 + +| 操作 | Windows | Linux | +|------|---------|-------| +| 安装服务 | `.\server.exe -reg install` | `sudo ./server -reg install` | +| 启动服务 | `sc start GoTunnelServer` | `sudo systemctl start GoTunnelServer` | +| 停止服务 | `sc stop GoTunnelServer` | `sudo systemctl stop GoTunnelServer` | +| 重启服务 | `sc stop ... && sc start ...` | `sudo systemctl restart GoTunnelServer` | +| 查看状态 | `sc query GoTunnelServer` | `sudo systemctl status GoTunnelServer` | +| 查看日志 | 事件查看器 | `sudo journalctl -u GoTunnelServer -f` | +| 卸载服务 | `.\server.exe -reg uninstall` | `sudo ./server -reg uninstall` | +| 开机自启 | 自动启用 | `sudo systemctl enable GoTunnelServer` | + +--- + +## 技术细节 + +### 自动重启机制 + +#### Windows +- 使用 Windows Service Recovery Options +- 服务崩溃时由 SCM(服务控制管理器)自动重启 +- 支持配置延迟和重试次数 + +#### Linux +- 使用 systemd 的 `Restart=always` 配置 +- 支持 `StartLimitBurst` 和 `StartLimitInterval` 防止重启循环 +- 系统级别的进程管理和监控 + +### 优雅关闭 + +两个平台都实现了优雅关闭: +1. 接收停止信号(SIGTERM/SIGINT) +2. 停止接受新连接 +3. 等待现有连接完成 +4. 清理资源(关闭数据库、停止转发器) +5. 退出程序 + +--- + +## 更新说明 + +更新服务时的步骤: + +### Windows +```powershell +# 1. 停止服务 +sc stop GoTunnelServer + +# 2. 替换可执行文件 +Copy-Item new-server.exe server.exe -Force + +# 3. 启动服务 +sc start GoTunnelServer +``` + +### Linux +```bash +# 1. 停止服务 +sudo systemctl stop GoTunnelServer + +# 2. 替换可执行文件 +sudo cp new-server /usr/local/bin/server + +# 3. 启动服务 +sudo systemctl start GoTunnelServer +``` + +--- + +## 支持和反馈 + +如有问题或建议,请提交 Issue 或 Pull Request。 diff --git a/src/client/main.go b/src/client/main.go index 4a84d58..2981942 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -2,67 +2,183 @@ package main import ( "flag" + "fmt" "log" "os" "os/signal" + "path/filepath" "port-forward/client/tunnel" + "port-forward/common/service" + "strings" "syscall" ) +// clientService 客户端服务实例 +type clientService struct { + serverAddr string + client *tunnel.Client + sigChan chan os.Signal +} + +func (s *clientService) Start() error { + log.Println("客户端服务启动中...") + + // 创建隧道客户端 + log.Printf("隧道客户端启动...") + log.Printf("服务器地址: %s", s.serverAddr) + + s.client = tunnel.NewClient(s.serverAddr) + + // 启动客户端 + if err := s.client.Start(); err != nil { + return fmt.Errorf("启动隧道客户端失败: %v", err) + } + + log.Println("===========================================") + log.Println("隧道客户端运行中...") + log.Println("===========================================") + + // 等待中断信号 + s.sigChan = make(chan os.Signal, 1) + signal.Notify(s.sigChan, os.Interrupt, syscall.SIGTERM) + <-s.sigChan + + return nil +} + +func (s *clientService) Stop() error { + log.Println("接收到关闭信号,正在关闭...") + + // 停止客户端 + if s.client != nil { + if err := s.client.Stop(); err != nil { + log.Printf("停止客户端失败: %v", err) + return err + } + } + + log.Println("客户端已关闭") + return nil +} + func main() { // 解析命令行参数 serverAddr := flag.String("server", "localhost:9000", "隧道服务器地址 (host:port)") + regAction := flag.String("reg", "", "服务管理: install(安装), uninstall(卸载), start(启动), stop(停止), status(状态)") flag.Parse() + // 设置日志格式 log.SetFlags(log.LstdFlags | log.Lshortfile) - // 创建隧道客户端 - log.Printf("隧道客户端启动...") - log.Printf("服务器地址: %s", *serverAddr) - - client := tunnel.NewClient(*serverAddr) - - // 启动客户端 - if err := client.Start(); err != nil { - log.Fatalf("启动隧道客户端失败: %v", err) + // 处理服务注册相关操作 + if *regAction != "" { + handleServiceAction(*regAction, *serverAddr) + return } - // // 启动 pprof 调试服务器(用于性能分析和调试) - // pprofPort := 6061 - // go func() { - // log.Printf("启动 pprof 调试服务器: http://localhost:%d/debug/pprof/", pprofPort) - // if err := http.ListenAndServe(":6061", nil); err != nil { - // log.Printf("pprof 服务器启动失败: %v", err) - // } - // }() - - // // 启动 goroutine 监控 - // go func() { - // ticker := time.NewTicker(10 * time.Second) - // defer ticker.Stop() - // for range ticker.C { - // numGoroutines := runtime.NumGoroutine() - // log.Printf("[监控] 当前 Goroutine 数量: %d", numGoroutines) - // } - // }() - - log.Println("===========================================") - log.Println("隧道客户端运行中...") - // log.Printf("调试接口: http://localhost:%d/debug/pprof/", pprofPort) - log.Println("按 Ctrl+C 退出") - log.Println("===========================================") - - // 等待中断信号 - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - <-sigChan - log.Println("\n接收到关闭信号,正在关闭...") - - // 停止客户端 - if err := client.Stop(); err != nil { - log.Printf("停止客户端失败: %v", err) + // 检查是否以服务模式运行(编译时根据平台自动选择实现) + if service.IsService() { + // 以服务模式运行 + runAsService(*serverAddr) + return } - log.Println("客户端已关闭") -} \ No newline at end of file + // 正常运行模式 + normalRun(*serverAddr) +} + +func normalRun(serverAddr string) { + svc := &clientService{ + serverAddr: serverAddr, + } + + if err := svc.Start(); err != nil { + log.Fatalf("客户端启动失败: %v", err) + } + + svc.Stop() +} + +func runAsService(serverAddr string) { + svc := &clientService{ + serverAddr: serverAddr, + } + + // 创建服务包装器 + wrapper := service.NewWindowsServiceWrapper(svc) + + // 运行服务 + err := service.RunAsService("GoTunnelClient", wrapper) + if err != nil { + log.Fatalf("运行服务失败: %v", err) + } +} + +func handleServiceAction(action, serverAddr string) { + // 检查管理员权限 + if !service.IsAdmin() { + log.Fatal("需要管理员/root权限来执行此操作") + } + + // 获取可执行文件路径 + exePath, err := os.Executable() + if err != nil { + log.Fatalf("获取可执行文件路径失败: %v", err) + } + exePath, err = filepath.Abs(exePath) + if err != nil { + log.Fatalf("获取绝对路径失败: %v", err) + } + + // 准备服务配置 + cfg := service.ServiceConfig{ + Name: "GoTunnelClient", + DisplayName: "Go Tunnel Client", + Description: "Go Tunnel 隧道客户端 - 连接到隧道服务器并提供内网穿透服务", + ExecPath: exePath, + Args: buildServiceArgs(serverAddr), + WorkingDir: filepath.Dir(exePath), + } + + // 创建服务实例 + svcInstance, err := service.NewService(cfg) + if err != nil { + log.Fatalf("创建服务实例失败: %v", err) + } + + // 执行操作 + switch strings.ToLower(action) { + case "install": + err = svcInstance.Install() + case "uninstall": + err = svcInstance.Uninstall() + case "start": + err = svcInstance.Start() + case "stop": + err = svcInstance.Stop() + case "status": + status, err := svcInstance.Status() + if err != nil { + log.Fatalf("查询服务状态失败: %v", err) + } + fmt.Printf("服务状态: %s\n", status) + return + default: + log.Fatalf("未知操作: %s。支持的操作: install, uninstall, start, stop, status", action) + } + + if err != nil { + log.Fatalf("执行操作 %s 失败: %v", action, err) + } +} + +func buildServiceArgs(serverAddr string) []string { + args := []string{} + + // 只添加非默认的服务器地址 + if serverAddr != "" && serverAddr != "localhost:9000" { + args = append(args, "-server", serverAddr) + } + + return args +} diff --git a/src/common/service/service.go b/src/common/service/service.go new file mode 100644 index 0000000..cac9aae --- /dev/null +++ b/src/common/service/service.go @@ -0,0 +1,62 @@ +package service + +import ( + "fmt" + "os" + "path/filepath" +) + +// ServiceConfig 服务配置 +type ServiceConfig struct { + Name string // 服务名称 + DisplayName string // 服务显示名称 + Description string // 服务描述 + ExecPath string // 可执行文件路径 + Args []string // 启动参数 + WorkingDir string // 工作目录 +} + +// Service 服务接口 +type Service interface { + // Install 安装服务 + Install() error + // Uninstall 卸载服务 + Uninstall() error + // Start 启动服务 + Start() error + // Stop 停止服务 + Stop() error + // Status 查询服务状态 + Status() (string, error) +} + +// NewService 创建服务实例 +func NewService(config ServiceConfig) (Service, error) { + // 如果没有指定可执行文件路径,使用当前程序路径 + if config.ExecPath == "" { + execPath, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("获取可执行文件路径失败: %v", err) + } + config.ExecPath, err = filepath.Abs(execPath) + if err != nil { + return nil, fmt.Errorf("获取绝对路径失败: %v", err) + } + } + + // 如果没有指定工作目录,使用可执行文件所在目录 + if config.WorkingDir == "" { + config.WorkingDir = filepath.Dir(config.ExecPath) + } + + // 调用平台特定的实现(编译时自动选择) + return newPlatformService(config) +} + +// newPlatformService 创建平台特定的服务实例 +// 该函数在 service_windows.go、service_linux.go、service_unsupported.go 中实现 +// 编译时会根据 build tags 自动选择对应平台的实现 + +// IsAdmin 检查当前用户是否有管理员权限 +// 该函数在 service_windows.go、service_linux.go、service_unsupported.go 中实现 +// 编译时会根据 build tags 自动选择对应平台的实现 diff --git a/src/common/service/service_linux.go b/src/common/service/service_linux.go new file mode 100644 index 0000000..b8c0758 --- /dev/null +++ b/src/common/service/service_linux.go @@ -0,0 +1,220 @@ +//go:build linux +// +build linux + +package service + +import ( + "fmt" + "os" + "os/exec" + "strings" + "text/template" +) + +type linuxService struct { + config ServiceConfig +} + +// newPlatformService 创建 Linux 服务实例 +func newPlatformService(config ServiceConfig) (Service, error) { + return &linuxService{config: config}, nil +} + +// IsAdmin 检查当前用户是否有管理员权限(Linux) +func IsAdmin() bool { + return os.Geteuid() == 0 +} + +// IsService 检查是否以服务模式运行(Linux 上始终返回 false) +// Linux 的 systemd 服务不需要特殊的服务模式,直接以普通模式运行即可 +func IsService() bool { + return false +} + +// WindowsServiceHandler 在 Linux 上的占位接口 +type WindowsServiceHandler interface { + Start() error + Stop() error +} + +// NewWindowsServiceWrapper 在 Linux 上不可用 +func NewWindowsServiceWrapper(handler WindowsServiceHandler) interface{} { + panic("NewWindowsServiceWrapper is not available on Linux") +} + +// RunAsService 在 Linux 上不可用 +func RunAsService(name string, handler interface{}) error { + return fmt.Errorf("RunAsService is not available on Linux") +} + +func (s *linuxService) Install() error { + // 生成 systemd service 文件 + serviceContent, err := s.generateServiceFile() + if err != nil { + return fmt.Errorf("生成服务配置文件失败: %v", err) + } + + // 服务文件路径 + serviceFilePath := fmt.Sprintf("/etc/systemd/system/%s.service", s.config.Name) + + // 写入服务文件 + err = os.WriteFile(serviceFilePath, []byte(serviceContent), 0644) + if err != nil { + return fmt.Errorf("写入服务配置文件失败: %v", err) + } + + // 重新加载 systemd + cmd := exec.Command("systemctl", "daemon-reload") + if err := cmd.Run(); err != nil { + return fmt.Errorf("重新加载 systemd 失败: %v", err) + } + + // 启用服务(开机自启) + cmd = exec.Command("systemctl", "enable", s.config.Name) + if err := cmd.Run(); err != nil { + return fmt.Errorf("启用服务失败: %v", err) + } + + fmt.Printf("服务 %s 安装成功\n", s.config.DisplayName) + fmt.Printf("服务文件: %s\n", serviceFilePath) + fmt.Printf("可执行文件: %s\n", s.config.ExecPath) + fmt.Printf("启动参数: %s\n", strings.Join(s.config.Args, " ")) + fmt.Printf("工作目录: %s\n", s.config.WorkingDir) + fmt.Println("\n使用以下命令管理服务:") + fmt.Printf(" 启动服务: sudo systemctl start %s\n", s.config.Name) + fmt.Printf(" 停止服务: sudo systemctl stop %s\n", s.config.Name) + fmt.Printf(" 查看状态: sudo systemctl status %s\n", s.config.Name) + fmt.Printf(" 查看日志: sudo journalctl -u %s -f\n", s.config.Name) + fmt.Printf(" 卸载服务: sudo %s -reg uninstall\n", s.config.ExecPath) + + return nil +} + +func (s *linuxService) Uninstall() error { + // 停止服务 + cmd := exec.Command("systemctl", "stop", s.config.Name) + if err := cmd.Run(); err != nil { + fmt.Printf("警告: 停止服务失败: %v\n", err) + } + + // 禁用服务 + cmd = exec.Command("systemctl", "disable", s.config.Name) + if err := cmd.Run(); err != nil { + fmt.Printf("警告: 禁用服务失败: %v\n", err) + } + + // 删除服务文件 + serviceFilePath := fmt.Sprintf("/etc/systemd/system/%s.service", s.config.Name) + err := os.Remove(serviceFilePath) + if err != nil { + return fmt.Errorf("删除服务配置文件失败: %v", err) + } + + // 重新加载 systemd + cmd = exec.Command("systemctl", "daemon-reload") + if err := cmd.Run(); err != nil { + return fmt.Errorf("重新加载 systemd 失败: %v", err) + } + + // 重置失败状态 + cmd = exec.Command("systemctl", "reset-failed") + cmd.Run() // 忽略错误 + + fmt.Printf("服务 %s 卸载成功\n", s.config.DisplayName) + return nil +} + +func (s *linuxService) Start() error { + cmd := exec.Command("systemctl", "start", s.config.Name) + if err := cmd.Run(); err != nil { + return fmt.Errorf("启动服务失败: %v", err) + } + + fmt.Printf("服务 %s 启动成功\n", s.config.DisplayName) + return nil +} + +func (s *linuxService) Stop() error { + cmd := exec.Command("systemctl", "stop", s.config.Name) + if err := cmd.Run(); err != nil { + return fmt.Errorf("停止服务失败: %v", err) + } + + fmt.Printf("服务 %s 停止成功\n", s.config.DisplayName) + return nil +} + +func (s *linuxService) Status() (string, error) { + cmd := exec.Command("systemctl", "is-active", s.config.Name) + output, err := cmd.Output() + if err != nil { + // 即使服务不活跃也会返回错误,所以检查输出 + status := strings.TrimSpace(string(output)) + if status != "" { + return status, nil + } + return "", fmt.Errorf("查询服务状态失败: %v", err) + } + + return strings.TrimSpace(string(output)), nil +} + +func (s *linuxService) generateServiceFile() (string, error) { + // systemd service 文件模板 + tmpl := `[Unit] +Description={{.Description}} +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory={{.WorkingDir}} +ExecStart={{.ExecStart}} +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal + +# 安全设置 +LimitNOFILE=65536 +LimitNPROC=65536 + +# 性能和资源限制 +TimeoutStartSec=0 +TimeoutStopSec=30 + +# 失败处理 +StartLimitInterval=60 +StartLimitBurst=5 + +[Install] +WantedBy=multi-user.target +` + + // 构建完整的启动命令 + execStart := s.config.ExecPath + if len(s.config.Args) > 0 { + execStart += " " + strings.Join(s.config.Args, " ") + } + + // 准备模板数据 + data := map[string]string{ + "Description": s.config.Description, + "WorkingDir": s.config.WorkingDir, + "ExecStart": execStart, + } + + // 解析并执行模板 + t, err := template.New("service").Parse(tmpl) + if err != nil { + return "", err + } + + var buf strings.Builder + err = t.Execute(&buf, data) + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/src/common/service/service_unsupported.go b/src/common/service/service_unsupported.go new file mode 100644 index 0000000..59d54e5 --- /dev/null +++ b/src/common/service/service_unsupported.go @@ -0,0 +1,44 @@ +//go:build !windows && !linux +// +build !windows,!linux + +package service + +import ( + "fmt" + "runtime" +) + +type unsupportedService struct { + config ServiceConfig +} + +// newPlatformService 不支持的平台 +func newPlatformService(config ServiceConfig) (Service, error) { + return nil, fmt.Errorf("不支持的操作系统: %s", runtime.GOOS) +} + +// IsAdmin 不支持的平台 +func IsAdmin() bool { + return false +} + +// IsService 检查是否以服务模式运行(不支持的平台上始终返回 false) +func IsService() bool { + return false +} + +// WindowsServiceHandler 在不支持的平台上的占位接口 +type WindowsServiceHandler interface { + Start() error + Stop() error +} + +// NewWindowsServiceWrapper 在不支持的平台上不可用 +func NewWindowsServiceWrapper(handler WindowsServiceHandler) interface{} { + panic("NewWindowsServiceWrapper is not available on this platform") +} + +// RunAsService 在不支持的平台上不可用 +func RunAsService(name string, handler interface{}) error { + return fmt.Errorf("RunAsService is not available on this platform") +} diff --git a/src/common/service/service_windows.go b/src/common/service/service_windows.go new file mode 100644 index 0000000..91aff4b --- /dev/null +++ b/src/common/service/service_windows.go @@ -0,0 +1,275 @@ +//go:build windows +// +build windows + +package service + +import ( + "fmt" + "strings" + "time" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" +) + +type windowsService struct { + config ServiceConfig +} + +// newPlatformService 创建 Windows 服务实例 +func newPlatformService(config ServiceConfig) (Service, error) { + return &windowsService{config: config}, nil +} + +func (s *windowsService) Install() error { + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("连接服务管理器失败: %v", err) + } + defer m.Disconnect() + + // 检查服务是否已存在 + service, err := m.OpenService(s.config.Name) + if err == nil { + service.Close() + return fmt.Errorf("服务 %s 已存在", s.config.Name) + } + + // 构建完整的启动命令 + exePath := s.config.ExecPath + args := s.config.Args + + // 创建服务配置 + config := mgr.Config{ + DisplayName: s.config.DisplayName, + Description: s.config.Description, + StartType: mgr.StartAutomatic, // 自动启动 + ServiceStartName: "", // 使用 LocalSystem 账户 + // 依赖项(可选) + Dependencies: []string{}, + } + + // 创建服务 + service, err = m.CreateService(s.config.Name, exePath, config, args...) + if err != nil { + return fmt.Errorf("创建服务失败: %v", err) + } + defer service.Close() + + // 设置服务恢复选项(失败后自动重启) + err = setRecoveryOptions(service) + if err != nil { + // 不阻止安装,仅记录警告 + fmt.Printf("警告: 设置服务恢复选项失败: %v\n", err) + } + + fmt.Printf("服务 %s 安装成功\n", s.config.DisplayName) + fmt.Printf("可执行文件: %s\n", exePath) + fmt.Printf("启动参数: %s\n", strings.Join(args, " ")) + fmt.Printf("工作目录: %s\n", s.config.WorkingDir) + fmt.Println("\n使用以下命令管理服务:") + fmt.Printf(" 启动服务: sc start %s\n", s.config.Name) + fmt.Printf(" 停止服务: sc stop %s\n", s.config.Name) + fmt.Printf(" 查询状态: sc query %s\n", s.config.Name) + fmt.Printf(" 卸载服务: %s -reg uninstall\n", exePath) + + return nil +} + +func (s *windowsService) Uninstall() error { + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("连接服务管理器失败: %v", err) + } + defer m.Disconnect() + + service, err := m.OpenService(s.config.Name) + if err != nil { + return fmt.Errorf("打开服务失败: %v", err) + } + defer service.Close() + + // 先停止服务 + status, err := service.Query() + if err != nil { + return fmt.Errorf("查询服务状态失败: %v", err) + } + + if status.State != svc.Stopped { + fmt.Println("正在停止服务...") + _, err = service.Control(svc.Stop) + if err != nil { + fmt.Printf("警告: 停止服务失败: %v\n", err) + } else { + // 等待服务停止 + for i := 0; i < 30; i++ { + status, err = service.Query() + if err != nil { + break + } + if status.State == svc.Stopped { + break + } + time.Sleep(time.Second) + } + } + } + + // 删除服务 + err = service.Delete() + if err != nil { + return fmt.Errorf("删除服务失败: %v", err) + } + + fmt.Printf("服务 %s 卸载成功\n", s.config.DisplayName) + return nil +} + +func (s *windowsService) Start() error { + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("连接服务管理器失败: %v", err) + } + defer m.Disconnect() + + service, err := m.OpenService(s.config.Name) + if err != nil { + return fmt.Errorf("打开服务失败: %v", err) + } + defer service.Close() + + err = service.Start() + if err != nil { + return fmt.Errorf("启动服务失败: %v", err) + } + + fmt.Printf("服务 %s 启动成功\n", s.config.DisplayName) + return nil +} + +func (s *windowsService) Stop() error { + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("连接服务管理器失败: %v", err) + } + defer m.Disconnect() + + service, err := m.OpenService(s.config.Name) + if err != nil { + return fmt.Errorf("打开服务失败: %v", err) + } + defer service.Close() + + _, err = service.Control(svc.Stop) + if err != nil { + return fmt.Errorf("停止服务失败: %v", err) + } + + fmt.Printf("服务 %s 停止成功\n", s.config.DisplayName) + return nil +} + +func (s *windowsService) Status() (string, error) { + m, err := mgr.Connect() + if err != nil { + return "", fmt.Errorf("连接服务管理器失败: %v", err) + } + defer m.Disconnect() + + service, err := m.OpenService(s.config.Name) + if err != nil { + return "", fmt.Errorf("打开服务失败: %v", err) + } + defer service.Close() + + status, err := service.Query() + if err != nil { + return "", fmt.Errorf("查询服务状态失败: %v", err) + } + + return stateToString(status.State), nil +} + +func stateToString(state svc.State) string { + switch state { + case svc.Stopped: + return "已停止" + case svc.StartPending: + return "正在启动" + case svc.StopPending: + return "正在停止" + case svc.Running: + return "运行中" + case svc.ContinuePending: + return "继续挂起" + case svc.PausePending: + return "暂停挂起" + case svc.Paused: + return "已暂停" + default: + return "未知" + } +} + +// setRecoveryOptions 设置服务失败恢复选项 +func setRecoveryOptions(service *mgr.Service) error { + // 第一次失败:1分钟后重启 + // 第二次失败:1分钟后重启 + // 后续失败:1分钟后重启 + // 重置失败计数:24小时 + actions := []mgr.RecoveryAction{ + {Type: mgr.ServiceRestart, Delay: 60 * time.Second}, + {Type: mgr.ServiceRestart, Delay: 60 * time.Second}, + {Type: mgr.ServiceRestart, Delay: 60 * time.Second}, + } + + return service.SetRecoveryActions(actions, 24*60*60) // 24小时重置 +} + +// IsAdmin 检查是否具有管理员权限(Windows) +func IsAdmin() bool { + var sid *windows.SID + err := windows.AllocateAndInitializeSid( + &windows.SECURITY_NT_AUTHORITY, + 2, + windows.SECURITY_BUILTIN_DOMAIN_RID, + windows.DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + &sid) + if err != nil { + return false + } + defer windows.FreeSid(sid) + + token := windows.Token(0) + member, err := token.IsMember(sid) + if err != nil { + return false + } + + return member +} + +// RunAsService 以服务模式运行 +func RunAsService(name string, handler svc.Handler) error { + isService, err := svc.IsWindowsService() + if err != nil { + return fmt.Errorf("检查服务模式失败: %v", err) + } + + if !isService { + return fmt.Errorf("不是以服务模式运行") + } + + return svc.Run(name, handler) +} + +// IsService 检查是否以服务模式运行(Windows) +func IsService() bool { + isService, err := svc.IsWindowsService() + if err != nil { + return false + } + return isService +} diff --git a/src/common/service/wrapper_windows.go b/src/common/service/wrapper_windows.go new file mode 100644 index 0000000..ef66375 --- /dev/null +++ b/src/common/service/wrapper_windows.go @@ -0,0 +1,77 @@ +//go:build windows +// +build windows + +package service + +import ( + "log" + + "golang.org/x/sys/windows/svc" +) + +// WindowsServiceHandler Windows 服务处理器接口 +type WindowsServiceHandler interface { + // Start 启动服务逻辑 + Start() error + // Stop 停止服务逻辑 + Stop() error +} + +// windowsServiceWrapper Windows 服务包装器 +type windowsServiceWrapper struct { + handler WindowsServiceHandler +} + +// NewWindowsServiceWrapper 创建 Windows 服务包装器 +func NewWindowsServiceWrapper(handler WindowsServiceHandler) svc.Handler { + return &windowsServiceWrapper{handler: handler} +} + +// Execute 实现 svc.Handler 接口 +func (w *windowsServiceWrapper) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + + // 通知服务控制管理器服务正在启动 + changes <- svc.Status{State: svc.StartPending} + + // 启动服务 + err := w.handler.Start() + if err != nil { + log.Printf("启动服务失败: %v", err) + return true, 1 + } + + // 通知服务控制管理器服务已启动 + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + + log.Println("服务已启动,等待控制命令...") + +loop: + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + // 服务状态查询 + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + // 收到停止命令 + log.Println("收到停止命令,正在关闭服务...") + changes <- svc.Status{State: svc.StopPending} + + // 停止服务 + err := w.handler.Stop() + if err != nil { + log.Printf("停止服务失败: %v", err) + } + + // 通知服务已停止 + break loop + default: + log.Printf("收到未知控制命令: %v", c.Cmd) + } + } + } + + return false, 0 +} diff --git a/src/go.mod b/src/go.mod index 1c0ee2e..03f99d3 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,8 +1,12 @@ module port-forward -go 1.21 +go 1.24.0 + +toolchain go1.24.4 require ( github.com/mattn/go-sqlite3 v1.14.22 gopkg.in/yaml.v3 v3.0.1 ) + +require golang.org/x/sys v0.37.0 // indirect diff --git a/src/go.sum b/src/go.sum index 2de29e1..19b802d 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,5 +1,7 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/server/main.go b/src/server/main.go index 302d8e1..cd8fc23 100644 --- a/src/server/main.go +++ b/src/server/main.go @@ -2,86 +2,91 @@ package main import ( "flag" + "fmt" "log" "os" "os/signal" + "path/filepath" + "port-forward/common/service" "port-forward/server/api" "port-forward/server/config" "port-forward/server/db" "port-forward/server/forwarder" "port-forward/server/tunnel" "port-forward/server/utils" + "strings" "syscall" ) -func main() { - // 解析命令行参数 - configPath := flag.String("config", "config.yaml", "配置文件路径") - flag.Parse() +// serverService 服务实例 +type serverService struct { + configPath string + cfg *config.Config + database *db.Database + fwdManager *forwarder.Manager + tunnelServer *tunnel.Server + apiHandler *api.Handler + sigChan chan os.Signal +} + +func (s *serverService) Start() error { + log.Println("服务启动中...") // 加载配置 log.Println("加载配置文件...") - cfg, err := config.Load(*configPath) + cfg, err := config.Load(s.configPath) if err != nil { - log.Fatalf("加载配置失败: %v", err) + return fmt.Errorf("加载配置失败: %v", err) } + s.cfg = cfg // 初始化数据库 log.Println("初始化数据库...") database, err := db.New(cfg.Database.Path) if err != nil { - log.Fatalf("初始化数据库失败: %v", err) + return fmt.Errorf("初始化数据库失败: %v", err) } - defer database.Close() + s.database = database // 创建转发器管理器 log.Println("创建转发器管理器...") - fwdManager := forwarder.NewManager() + s.fwdManager = forwarder.NewManager() // 如果启用隧道,启动隧道服务器 - var tunnelServer *tunnel.Server if cfg.Tunnel.Enabled { log.Println("启动隧道服务器...") - tunnelServer = tunnel.NewServer(cfg.Tunnel.ListenPort) - if err := tunnelServer.Start(); err != nil { - log.Fatalf("启动隧道服务器失败: %v", err) + s.tunnelServer = tunnel.NewServer(cfg.Tunnel.ListenPort) + if err := s.tunnelServer.Start(); err != nil { + return fmt.Errorf("启动隧道服务器失败: %v", err) } - defer tunnelServer.Stop() } // 从数据库加载现有映射并启动转发器 log.Println("加载现有端口映射...") mappings, err := database.GetAllMappings() if err != nil { - log.Fatalf("加载端口映射失败: %v", err) + return fmt.Errorf("加载端口映射失败: %v", err) } for _, mapping := range mappings { - // 验证端口在范围内 - // if mapping.SourcePort < cfg.PortRange.From || mapping.SourcePort > cfg.PortRange.End { - // log.Printf("警告: 端口 %d 超出范围,跳过", mapping.SourcePort) - // continue - // } - used := utils.PortCheck(mapping.SourcePort) - if used { - log.Fatalf("警告: 端口 %d 已被占! 将退出程序", mapping.SourcePort) + return fmt.Errorf("端口 %d 已被占用", mapping.SourcePort) } var err error if mapping.UseTunnel { // 隧道模式:检查隧道服务器是否可用 - if !cfg.Tunnel.Enabled || tunnelServer == nil { + if !cfg.Tunnel.Enabled || s.tunnelServer == nil { log.Printf("警告: 端口 %d 需要隧道模式但隧道服务未启用,跳过", mapping.SourcePort) continue } - err = fwdManager.AddTunnel(mapping.SourcePort, mapping.TargetHost, mapping.TargetPort, tunnelServer) + err = s.fwdManager.AddTunnel(mapping.SourcePort, mapping.TargetHost, mapping.TargetPort, s.tunnelServer) } else { // 直接模式 - err = fwdManager.Add(mapping.SourcePort, mapping.TargetHost, mapping.TargetPort) + err = s.fwdManager.Add(mapping.SourcePort, mapping.TargetHost, mapping.TargetPort) } - + if err != nil { log.Printf("警告: 启动端口 %d 的转发失败: %v", mapping.SourcePort, err) } @@ -91,61 +96,174 @@ func main() { // 创建 HTTP API 处理器 log.Println("初始化 HTTP API...") - apiHandler := api.NewHandler( - database, - fwdManager, - tunnelServer, - // cfg.PortRange.From, - // cfg.PortRange.End, - ) + s.apiHandler = api.NewHandler(database, s.fwdManager, s.tunnelServer) // 启动 HTTP API 服务器 go func() { - if err := api.Start(apiHandler, cfg.API.ListenPort); err != nil { + if err := api.Start(s.apiHandler, cfg.API.ListenPort); err != nil { log.Fatalf("启动 HTTP API 服务失败: %v", err) } }() - // // 启动 pprof 调试服务器(用于性能分析和调试) - // pprofPort := 6060 - // go func() { - // log.Printf("启动 pprof 调试服务器: http://localhost:%d/debug/pprof/", pprofPort) - // if err := http.ListenAndServe(":6060", nil); err != nil { - // log.Printf("pprof 服务器启动失败: %v", err) - // } - // }() - - // // 启动 goroutine 监控 - // go func() { - // ticker := time.NewTicker(10 * time.Second) - // defer ticker.Stop() - // for range ticker.C { - // numGoroutines := runtime.NumGoroutine() - // log.Printf("[监控] 当前 Goroutine 数量: %d", numGoroutines) - // } - // }() - log.Println("===========================================") log.Printf("服务器启动成功!") - // log.Printf("端口范围: %d-%d", cfg.PortRange.From, cfg.PortRange.End) log.Printf("HTTP API: http://localhost:%d", cfg.API.ListenPort) - // log.Printf("调试接口: http://localhost:%d/debug/pprof/", pprofPort) if cfg.Tunnel.Enabled { log.Printf("隧道服务: 端口 %d", cfg.Tunnel.ListenPort) } log.Println("===========================================") // 等待中断信号 - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + s.sigChan = make(chan os.Signal, 1) + signal.Notify(s.sigChan, os.Interrupt, syscall.SIGTERM) + <-s.sigChan - <-sigChan - log.Println("\n接收到关闭信号,正在优雅关闭...") + return nil +} + +func (s *serverService) Stop() error { + log.Println("接收到关闭信号,正在优雅关闭...") - // 创建关闭上下文 // 停止所有转发器 - log.Println("停止所有端口转发...") - fwdManager.StopAll() + if s.fwdManager != nil { + log.Println("停止所有端口转发...") + s.fwdManager.StopAll() + } + + // 停止隧道服务器 + if s.tunnelServer != nil { + log.Println("停止隧道服务器...") + s.tunnelServer.Stop() + } + + // 关闭数据库 + if s.database != nil { + log.Println("关闭数据库...") + s.database.Close() + } log.Println("服务器已关闭") -} \ No newline at end of file + return nil +} + +func main() { + // 解析命令行参数 + configPath := flag.String("config", "config.yaml", "配置文件路径") + regAction := flag.String("reg", "", "服务管理: install(安装), uninstall(卸载), start(启动), stop(停止), status(状态)") + flag.Parse() + + // 设置日志格式 + log.SetFlags(log.LstdFlags | log.Lshortfile) + + // 处理服务注册相关操作 + if *regAction != "" { + handleServiceAction(*regAction, *configPath) + return + } + + // 检查是否以服务模式运行(编译时根据平台自动选择实现) + if service.IsService() { + // 以服务模式运行 + runAsService(*configPath) + return + } + + // 正常运行模式 + normalRun(*configPath) +} + +func normalRun(configPath string) { + svc := &serverService{ + configPath: configPath, + } + + if err := svc.Start(); err != nil { + log.Fatalf("服务启动失败: %v", err) + } + + svc.Stop() +} + +func runAsService(configPath string) { + svc := &serverService{ + configPath: configPath, + } + + // 创建服务包装器 + wrapper := service.NewWindowsServiceWrapper(svc) + + // 运行服务 + err := service.RunAsService("GoTunnelServer", wrapper) + if err != nil { + log.Fatalf("运行服务失败: %v", err) + } +} + +func handleServiceAction(action, configPath string) { + // 检查管理员权限 + if !service.IsAdmin() { + log.Fatal("需要管理员/root权限来执行此操作") + } + + // 获取可执行文件路径 + exePath, err := os.Executable() + if err != nil { + log.Fatalf("获取可执行文件路径失败: %v", err) + } + exePath, err = filepath.Abs(exePath) + if err != nil { + log.Fatalf("获取绝对路径失败: %v", err) + } + + // 准备服务配置 + cfg := service.ServiceConfig{ + Name: "GoTunnelServer", + DisplayName: "Go Tunnel Server", + Description: "Go Tunnel 端口转发服务器 - 提供端口转发和隧道服务", + ExecPath: exePath, + Args: buildServiceArgs(configPath), + WorkingDir: filepath.Dir(exePath), + } + + // 创建服务实例 + svcInstance, err := service.NewService(cfg) + if err != nil { + log.Fatalf("创建服务实例失败: %v", err) + } + + // 执行操作 + switch strings.ToLower(action) { + case "install": + err = svcInstance.Install() + case "uninstall": + err = svcInstance.Uninstall() + case "start": + err = svcInstance.Start() + case "stop": + err = svcInstance.Stop() + case "status": + status, err := svcInstance.Status() + if err != nil { + log.Fatalf("查询服务状态失败: %v", err) + } + fmt.Printf("服务状态: %s\n", status) + return + default: + log.Fatalf("未知操作: %s。支持的操作: install, uninstall, start, stop, status", action) + } + + if err != nil { + log.Fatalf("执行操作 %s 失败: %v", action, err) + } +} + +func buildServiceArgs(configPath string) []string { + args := []string{} + + // 只添加非默认的配置路径 + if configPath != "" && configPath != "config.yaml" { + args = append(args, "-config", configPath) + } + + return args +}