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
@@ -3,6 +3,8 @@ package frp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -14,6 +16,17 @@ import (
|
||||
"vpn-platform/backend/internal/store"
|
||||
)
|
||||
|
||||
// findBin 查找 FRP 二进制路径:优先 data/bin/(平台下载),其次 PATH(系统安装)
|
||||
func findBin(name string) (string, bool) {
|
||||
// 1. data/bin/(平台下载部署的)
|
||||
managed := filepath.Join(service.BaseDir(), "bin", name)
|
||||
if fi, err := os.Stat(managed); err == nil && !fi.IsDir() {
|
||||
return managed, true
|
||||
}
|
||||
// 2. 系统 PATH
|
||||
return service.LookBin(name)
|
||||
}
|
||||
|
||||
type Service struct{}
|
||||
|
||||
// Proxy is the supported, repeatable FRP proxy rule. It is deliberately kept
|
||||
@@ -29,6 +42,7 @@ type Proxy struct {
|
||||
Subdomain string `json:"subdomain"`
|
||||
Locations []string `json:"locations"`
|
||||
HostHeaderRewrite string `json:"host_header_rewrite"`
|
||||
SecretKey string `json:"secret_key,omitempty"` // stcp/xtcp 访问密钥
|
||||
}
|
||||
|
||||
var safeName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$`)
|
||||
@@ -38,10 +52,13 @@ func (s *Service) Name() string { return "frp" }
|
||||
func (s *Service) DisplayName() string { return "FRP 内网穿透" }
|
||||
|
||||
func (s *Service) Available() (bool, string) {
|
||||
if p, ok := service.LookBin("frps", "frpc"); ok {
|
||||
if p, ok := findBin("frps"); ok {
|
||||
return true, p
|
||||
}
|
||||
return false, "未检测到 frps/frpc 二进制,可生成配置但无法启动"
|
||||
if p, ok := findBin("frpc"); ok {
|
||||
return true, p
|
||||
}
|
||||
return false, "未检测到 frps/frpc 二进制,点击「下载部署」可自动从 GitHub 下载"
|
||||
}
|
||||
|
||||
func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{}, error) {
|
||||
@@ -106,8 +123,10 @@ func validateProxy(p Proxy) error {
|
||||
if !safeName.MatchString(p.Name) {
|
||||
return fmt.Errorf("代理名称 %q 无效", p.Name)
|
||||
}
|
||||
if p.Type != "tcp" && p.Type != "udp" && p.Type != "http" && p.Type != "https" {
|
||||
return fmt.Errorf("代理 %s 的类型必须为 tcp、udp、http 或 https", p.Name)
|
||||
switch p.Type {
|
||||
case "tcp", "udp", "http", "https", "stcp", "xtcp":
|
||||
default:
|
||||
return fmt.Errorf("代理 %s 的类型必须为 tcp、udp、http、https、stcp 或 xtcp", p.Name)
|
||||
}
|
||||
if p.LocalPort < 1 || p.LocalPort > 65535 {
|
||||
return fmt.Errorf("代理 %s 的 local_port 无效", p.Name)
|
||||
@@ -124,6 +143,15 @@ func validateProxy(p Proxy) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if p.Type == "stcp" || p.Type == "xtcp" {
|
||||
if p.SecretKey == "" {
|
||||
return fmt.Errorf("STCP/XTCP 代理 %s 必须填写 secret_key", p.Name)
|
||||
}
|
||||
if strings.ContainsAny(p.SecretKey, "\r\n\"'") {
|
||||
return fmt.Errorf("代理 %s 的 secret_key 非法", p.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(p.CustomDomains) == 0 && p.Subdomain == "" {
|
||||
return fmt.Errorf("HTTP/HTTPS 代理 %s 必须填写 custom_domains 或 subdomain", p.Name)
|
||||
}
|
||||
@@ -218,9 +246,12 @@ func (s *Service) frpcConfig(cfg map[string]interface{}, token string) string {
|
||||
rules, _ := proxyRules(cfg)
|
||||
for _, p := range rules {
|
||||
fmt.Fprintf(&b, "\n[[proxies]]\nname = %q\ntype = %q\nlocalIP = %q\nlocalPort = %d\n", p.Name, p.Type, defaultString(p.LocalIP, "127.0.0.1"), p.LocalPort)
|
||||
if p.Type == "tcp" || p.Type == "udp" {
|
||||
switch p.Type {
|
||||
case "tcp", "udp":
|
||||
fmt.Fprintf(&b, "remotePort = %d\n", p.RemotePort)
|
||||
} else {
|
||||
case "stcp", "xtcp":
|
||||
fmt.Fprintf(&b, "secretKey = %q\n", p.SecretKey)
|
||||
default:
|
||||
if len(p.CustomDomains) > 0 {
|
||||
fmt.Fprintf(&b, "customDomains = [%s]\n", strings.Join(quoteStrings(p.CustomDomains), ", "))
|
||||
}
|
||||
@@ -262,10 +293,10 @@ func (s *Service) Start(t *model.Tunnel) error {
|
||||
if service.StrOr(cfg, "mode", "server") == "server" {
|
||||
bin, conf = "frps", "frps.toml"
|
||||
}
|
||||
if p, ok := service.LookBin(bin); ok {
|
||||
if p, ok := findBin(bin); ok {
|
||||
return service.StartDaemon(t, p, "-c", fmt.Sprintf("%s/%s", service.InstanceDir(t), conf))
|
||||
}
|
||||
return fmt.Errorf("无法启动 FRP:未检测到 %s", bin)
|
||||
return fmt.Errorf("无法启动 FRP:未检测到 %s,点击「下载部署」自动安装", bin)
|
||||
}
|
||||
func (s *Service) Stop(t *model.Tunnel) error { return service.KillPid(t) }
|
||||
func (s *Service) Status(t *model.Tunnel) (core.Status, error) {
|
||||
@@ -277,7 +308,7 @@ func (s *Service) Status(t *model.Tunnel) (core.Status, error) {
|
||||
if service.StrOr(cfg, "mode", "server") == "server" {
|
||||
bin = "frps"
|
||||
}
|
||||
if _, ok := service.LookBin(bin); !ok {
|
||||
if _, ok := findBin(bin); !ok {
|
||||
return service.NowStatus(false, "undeployed", fmt.Sprintf("配置已生成,但未检测到 %s,尚未部署", bin)), nil
|
||||
}
|
||||
return service.NowStatus(false, "stopped", "未启动"), nil
|
||||
@@ -296,7 +327,7 @@ func proxyRules(cfg map[string]interface{}) ([]Proxy, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("proxies 中每项必须是对象")
|
||||
}
|
||||
p := Proxy{Name: service.Str(m, "name"), Type: service.StrOr(m, "type", "tcp"), LocalIP: service.StrOr(m, "local_ip", "127.0.0.1"), LocalPort: int(service.Num(m, "local_port", 0)), RemotePort: int(service.Num(m, "remote_port", 0)), Subdomain: service.Str(m, "subdomain"), HostHeaderRewrite: service.Str(m, "host_header_rewrite")}
|
||||
p := Proxy{Name: service.Str(m, "name"), Type: service.StrOr(m, "type", "tcp"), LocalIP: service.StrOr(m, "local_ip", "127.0.0.1"), LocalPort: int(service.Num(m, "local_port", 0)), RemotePort: int(service.Num(m, "remote_port", 0)), Subdomain: service.Str(m, "subdomain"), HostHeaderRewrite: service.Str(m, "host_header_rewrite"), SecretKey: service.Str(m, "secret_key")}
|
||||
p.CustomDomains = stringList(m["custom_domains"])
|
||||
p.Locations = stringList(m["locations"])
|
||||
rules = append(rules, p)
|
||||
@@ -304,14 +335,14 @@ func proxyRules(cfg map[string]interface{}) ([]Proxy, error) {
|
||||
return rules, nil
|
||||
}
|
||||
if name := service.Str(cfg, "proxy_name"); name != "" {
|
||||
return []Proxy{{Name: name, Type: service.StrOr(cfg, "proxy_type", "tcp"), LocalIP: service.StrOr(cfg, "local_ip", "127.0.0.1"), LocalPort: int(service.Num(cfg, "local_port", 0)), RemotePort: int(service.Num(cfg, "remote_port", 0)), CustomDomains: splitDomains(service.Str(cfg, "custom_domains"))}}, nil
|
||||
return []Proxy{{Name: name, Type: service.StrOr(cfg, "proxy_type", "tcp"), LocalIP: service.StrOr(cfg, "local_ip", "127.0.0.1"), LocalPort: int(service.Num(cfg, "local_port", 0)), RemotePort: int(service.Num(cfg, "remote_port", 0)), CustomDomains: splitDomains(service.Str(cfg, "custom_domains")), SecretKey: service.Str(cfg, "secret_key")}}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func proxiesJSON(in []Proxy) []interface{} {
|
||||
out := make([]interface{}, 0, len(in))
|
||||
for _, p := range in {
|
||||
out = append(out, map[string]interface{}{"name": p.Name, "type": p.Type, "local_ip": p.LocalIP, "local_port": p.LocalPort, "remote_port": p.RemotePort, "custom_domains": p.CustomDomains, "subdomain": p.Subdomain, "locations": p.Locations, "host_header_rewrite": p.HostHeaderRewrite})
|
||||
out = append(out, map[string]interface{}{"name": p.Name, "type": p.Type, "local_ip": p.LocalIP, "local_port": p.LocalPort, "remote_port": p.RemotePort, "custom_domains": p.CustomDomains, "subdomain": p.Subdomain, "locations": p.Locations, "host_header_rewrite": p.HostHeaderRewrite, "secret_key": p.SecretKey})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user