Files
Logan.Li 1e2f8ef24f feat: 全面优化 — UI/UX重构 + 流量审计与会话管理 + 协议部署
UI/UX 优化:
- 登录页分屏布局 + 可访问性修复 + 内联错误提示
- 侧栏菜单分组(专线运维/资源管理/系统) + 图标修正 + 子路由高亮
- Dashboard 统计卡渐变图标 + 空状态引导 + 统计卡可点击
- 创建向导: 依赖检测门控 + 高级折叠 + 必填校验 + WireGuard推荐标
- 专线列表: 空状态CTA + 操作列「更多」下拉 + 状态筛选 + SSE日志流
- 协议品牌SVG图标(ProtocolIcon组件)替换emoji
- 设计令牌系统(tokens.css)统一颜色/间距/圆角/阴影

功能增强:
- 在线会话管理(sessionmgr): 跨协议会话同步 + 踢下线 + 连接审计日志
- 流量配额引擎(trafficmgr): 配额检查 + 超额动作(block/warn/throttle) + 周期重置
- 异常流量告警: 突发流量检测(>90MB/15s) + 审计记录
- 节点SSH凭证管理: 密码/私钥认证 + 即时测试连接 + AES-GCM加密存储
- 协议二进制管理(binmgr): FRP/OpenVPN GitHub下载 + 系统包安装引导(apt/yum/dnf)
- 右上角修改密码弹窗 + MFA独立验证页 + 密码强度提示
- 用户管理: 批量导入 + LDAP浏览导入 + 平台用户导入VPN拨号用户
- 404路由 + IPSec对接页可达性修复
- MFA备用码持久化弹窗(替代Toast)
- 轮询可见性守卫 + ECharts resize修复

后端:
- 3张新表: VPNSession/ConnectionLog/TrafficPolicy
- monitor增强: 会话同步+配额检查+异常告警+到期审计
- changePassword统一8位+新旧不同校验
- openvpn-otp文案专业化(移除抄袭感)
- SSE日志流(token降级query参数)
2026-07-23 01:42:36 +08:00

130 lines
4.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package sshutil 提供 SSH 连接测试能力,用于远程节点凭证验证。
// 借鉴 aiops pkg/remoteexec 的连接逻辑,精简为节点连通性测试专用。
// 基于 golang.org/x/crypto/ssh,支持密码认证与私钥认证。
package sshutil
import (
"fmt"
"net"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
// Config SSH 连接测试配置
type Config struct {
Host string // 目标 IP 或域名
Port int // SSH 端口(默认 22)
User string // SSH 用户(默认 root)
AuthType string // password / key
Password string // 密码认证
KeyData string // 私钥内容
}
// Result 连接测试结果
type Result struct {
Success bool `json:"success"`
Message string `json:"message"` // 成功时为 uname 输出,失败时为友好错误
Output string `json:"output"` // 原始命令输出
Took int64 `json:"took_ms"` // 耗时(毫秒)
}
// TestConnection 测试 SSH 连接并执行探测命令,返回友好结果。
// 探测命令:echo vpn-node-ok && uname -a(与 aiops build-node 一致)
func TestConnection(cfg Config) Result {
start := time.Now()
if cfg.Port == 0 {
cfg.Port = 22
}
if cfg.User == "" {
cfg.User = "root"
}
authMethods, err := buildAuth(cfg)
if err != nil {
return Result{Success: false, Message: err.Error(), Took: time.Since(start).Milliseconds()}
}
addr := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", cfg.Port))
sshConfig := &ssh.ClientConfig{
User: cfg.User,
Auth: authMethods,
Timeout: 10 * time.Second,
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 内网管理工具,简化主机密钥校验
}
client, err := ssh.Dial("tcp", addr, sshConfig)
if err != nil {
return Result{Success: false, Message: formatSSHError(err, addr), Took: time.Since(start).Milliseconds()}
}
defer client.Close()
// 执行探测命令
session, err := client.NewSession()
if err != nil {
return Result{Success: false, Message: fmt.Sprintf("创建会话失败: %v", err), Took: time.Since(start).Milliseconds()}
}
defer session.Close()
out, err := session.CombinedOutput("echo vpn-node-ok && uname -a")
output := strings.TrimSpace(string(out))
if err != nil {
return Result{Success: false, Message: fmt.Sprintf("命令执行失败: %v", err), Output: output, Took: time.Since(start).Milliseconds()}
}
return Result{
Success: true,
Message: output,
Output: output,
Took: time.Since(start).Milliseconds(),
}
}
// buildAuth 根据认证方式构建 ssh.AuthMethod 列表。
// 密钥认证解析失败时,若有密码则兜底用密码(aiops 模式)。
func buildAuth(cfg Config) ([]ssh.AuthMethod, error) {
switch cfg.AuthType {
case "key":
if cfg.KeyData == "" {
return nil, fmt.Errorf("私钥认证模式但未提供私钥")
}
signer, err := ssh.ParsePrivateKey([]byte(cfg.KeyData))
if err != nil {
// 私钥解析失败,尝试密码兜底
if cfg.Password != "" {
return []ssh.AuthMethod{ssh.Password(cfg.Password)}, nil
}
return nil, fmt.Errorf("私钥解析失败: %v", err)
}
return []ssh.AuthMethod{ssh.PublicKeys(signer)}, nil
default: // password
if cfg.Password == "" {
return nil, fmt.Errorf("密码认证模式但未提供密码")
}
return []ssh.AuthMethod{ssh.Password(cfg.Password)}, nil
}
}
// formatSSHError 将底层 SSH 错误映射为用户友好的中文提示
func formatSSHError(err error, addr string) string {
msg := err.Error()
switch {
case strings.Contains(msg, "i/o timeout"), strings.Contains(msg, "deadline exceeded"):
return fmt.Sprintf("连接超时:无法在 10 秒内连接到 %s,请检查地址/端口/防火墙", addr)
case strings.Contains(msg, "connection refused"):
return fmt.Sprintf("连接被拒绝:%s 端口未开放 SSH 服务", addr)
case strings.Contains(msg, "no route to host"):
return fmt.Sprintf("网络不可达:%s 地址不存在或路由不通", addr)
case strings.Contains(msg, "authentication failed"), strings.Contains(msg, "unable to authenticate"):
return "认证失败:用户名/密码/私钥不正确"
case strings.Contains(msg, "handshake"):
return fmt.Sprintf("SSH 握手失败:%s 可能不是标准 SSH 服务", addr)
case strings.Contains(msg, "dns"), strings.Contains(msg, "no such host"):
return fmt.Sprintf("域名解析失败:%s", addr)
default:
return fmt.Sprintf("连接失败:%s", msg)
}
}