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参数)
This commit is contained in:
1 parent
58c036c34c
commit
1e2f8ef24f
43 files changed
+6783
-460
No files matched your search
@@ -0,0 +1,129 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user