新增功能: - 新增 TLS 指纹 Profile CRUD 管理(Ent schema + 迁移 + Admin API + 前端管理界面) - 支持账号绑定数据库中的自定义 TLS Profile,或随机选择(profile_id=-1) - HTTPUpstream.DoWithTLS 接口从 bool 改为 *tlsfingerprint.Profile,支持按账号指定 Profile - AccountUsageService 注入 TLSFingerprintProfileService,统一 usage 场景与网关的 Profile 解析逻辑 代码优化: - 删除已被 TLSFingerprintProfileService 完全取代的 registry.go 死代码(418 行) - 提取 3 个 dialer 的重复 TLS 握手逻辑为 performTLSHandshake() 共用函数 - 修复 GetTLSFingerprintProfileID 缺少 json.Number 处理的 bug - gateway_service.Forward 中 ResolveTLSProfile 从重试循环内重复调用改为预解析局部变量 - 删除冗余的 buildClientHelloSpec() 单行 wrapper 和 int64(e.ID) 无效转换 - tls_fingerprint_profile_cache.go 日志从 log.Printf 改为 slog 结构化日志 - dialer_capture_test.go 添加 //go:build integration 标签,防止 CI 失败 - 去重 TestProfileExpectation 类型至共享 test_types_test.go - 修复 9 个测试文件缺少 tlsfingerprint import 的编译错误 - 修复 error_policy_integration_test.go 中 handleError 回调签名被错误替换的问题
260 lines
7.6 KiB
Go
260 lines
7.6 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"math/rand/v2"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/model"
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||
)
|
||
|
||
// TLSFingerprintProfileRepository 定义 TLS 指纹模板的数据访问接口
|
||
type TLSFingerprintProfileRepository interface {
|
||
List(ctx context.Context) ([]*model.TLSFingerprintProfile, error)
|
||
GetByID(ctx context.Context, id int64) (*model.TLSFingerprintProfile, error)
|
||
Create(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error)
|
||
Update(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error)
|
||
Delete(ctx context.Context, id int64) error
|
||
}
|
||
|
||
// TLSFingerprintProfileCache 定义 TLS 指纹模板的缓存接口
|
||
type TLSFingerprintProfileCache interface {
|
||
Get(ctx context.Context) ([]*model.TLSFingerprintProfile, bool)
|
||
Set(ctx context.Context, profiles []*model.TLSFingerprintProfile) error
|
||
Invalidate(ctx context.Context) error
|
||
NotifyUpdate(ctx context.Context) error
|
||
SubscribeUpdates(ctx context.Context, handler func())
|
||
}
|
||
|
||
// TLSFingerprintProfileService TLS 指纹模板管理服务
|
||
type TLSFingerprintProfileService struct {
|
||
repo TLSFingerprintProfileRepository
|
||
cache TLSFingerprintProfileCache
|
||
|
||
// 本地 ID→Profile 映射缓存,用于 DoWithTLS 热路径快速查找
|
||
localCache map[int64]*model.TLSFingerprintProfile
|
||
localMu sync.RWMutex
|
||
}
|
||
|
||
// NewTLSFingerprintProfileService 创建 TLS 指纹模板服务
|
||
func NewTLSFingerprintProfileService(
|
||
repo TLSFingerprintProfileRepository,
|
||
cache TLSFingerprintProfileCache,
|
||
) *TLSFingerprintProfileService {
|
||
svc := &TLSFingerprintProfileService{
|
||
repo: repo,
|
||
cache: cache,
|
||
localCache: make(map[int64]*model.TLSFingerprintProfile),
|
||
}
|
||
|
||
ctx := context.Background()
|
||
if err := svc.reloadFromDB(ctx); err != nil {
|
||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to load profiles from DB on startup: %v", err)
|
||
if fallbackErr := svc.refreshLocalCache(ctx); fallbackErr != nil {
|
||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to load profiles from cache fallback on startup: %v", fallbackErr)
|
||
}
|
||
}
|
||
|
||
if cache != nil {
|
||
cache.SubscribeUpdates(ctx, func() {
|
||
if err := svc.refreshLocalCache(context.Background()); err != nil {
|
||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to refresh cache on notification: %v", err)
|
||
}
|
||
})
|
||
}
|
||
|
||
return svc
|
||
}
|
||
|
||
// --- CRUD ---
|
||
|
||
// List 获取所有模板
|
||
func (s *TLSFingerprintProfileService) List(ctx context.Context) ([]*model.TLSFingerprintProfile, error) {
|
||
return s.repo.List(ctx)
|
||
}
|
||
|
||
// GetByID 根据 ID 获取模板
|
||
func (s *TLSFingerprintProfileService) GetByID(ctx context.Context, id int64) (*model.TLSFingerprintProfile, error) {
|
||
return s.repo.GetByID(ctx, id)
|
||
}
|
||
|
||
// Create 创建模板
|
||
func (s *TLSFingerprintProfileService) Create(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) {
|
||
if err := profile.Validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
created, err := s.repo.Create(ctx, profile)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
refreshCtx, cancel := s.newCacheRefreshContext()
|
||
defer cancel()
|
||
s.invalidateAndNotify(refreshCtx)
|
||
|
||
return created, nil
|
||
}
|
||
|
||
// Update 更新模板
|
||
func (s *TLSFingerprintProfileService) Update(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) {
|
||
if err := profile.Validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
updated, err := s.repo.Update(ctx, profile)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
refreshCtx, cancel := s.newCacheRefreshContext()
|
||
defer cancel()
|
||
s.invalidateAndNotify(refreshCtx)
|
||
|
||
return updated, nil
|
||
}
|
||
|
||
// Delete 删除模板
|
||
func (s *TLSFingerprintProfileService) Delete(ctx context.Context, id int64) error {
|
||
if err := s.repo.Delete(ctx, id); err != nil {
|
||
return err
|
||
}
|
||
|
||
refreshCtx, cancel := s.newCacheRefreshContext()
|
||
defer cancel()
|
||
s.invalidateAndNotify(refreshCtx)
|
||
|
||
return nil
|
||
}
|
||
|
||
// --- 热路径:运行时 Profile 查找 ---
|
||
|
||
// GetProfileByID 根据 ID 从本地缓存获取 Profile(用于 DoWithTLS 热路径)
|
||
// 返回 nil 表示未找到,调用方应 fallback 到内置默认 Profile
|
||
func (s *TLSFingerprintProfileService) GetProfileByID(id int64) *tlsfingerprint.Profile {
|
||
s.localMu.RLock()
|
||
p, ok := s.localCache[id]
|
||
s.localMu.RUnlock()
|
||
|
||
if ok && p != nil {
|
||
return p.ToTLSProfile()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// getRandomProfile 从本地缓存中随机选择一个 Profile
|
||
func (s *TLSFingerprintProfileService) getRandomProfile() *tlsfingerprint.Profile {
|
||
s.localMu.RLock()
|
||
defer s.localMu.RUnlock()
|
||
|
||
if len(s.localCache) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 收集所有 profile
|
||
profiles := make([]*model.TLSFingerprintProfile, 0, len(s.localCache))
|
||
for _, p := range s.localCache {
|
||
if p != nil {
|
||
profiles = append(profiles, p)
|
||
}
|
||
}
|
||
if len(profiles) == 0 {
|
||
return nil
|
||
}
|
||
|
||
return profiles[rand.IntN(len(profiles))].ToTLSProfile()
|
||
}
|
||
|
||
// ResolveTLSProfile 根据 Account 的配置解析出运行时 TLS Profile
|
||
//
|
||
// 逻辑:
|
||
// 1. 未启用 TLS 指纹 → 返回 nil(不伪装)
|
||
// 2. 启用 + 绑定了 profile_id → 从缓存查找对应 profile
|
||
// 3. 启用 + 未绑定或找不到 → 返回空 Profile(使用代码内置默认值)
|
||
func (s *TLSFingerprintProfileService) ResolveTLSProfile(account *Account) *tlsfingerprint.Profile {
|
||
if account == nil || !account.IsTLSFingerprintEnabled() {
|
||
return nil
|
||
}
|
||
id := account.GetTLSFingerprintProfileID()
|
||
if id > 0 {
|
||
if p := s.GetProfileByID(id); p != nil {
|
||
return p
|
||
}
|
||
}
|
||
if id == -1 {
|
||
// 随机选择一个 profile
|
||
if p := s.getRandomProfile(); p != nil {
|
||
return p
|
||
}
|
||
}
|
||
// TLS 启用但无绑定 profile → 空 Profile → dialer 使用内置默认值
|
||
return &tlsfingerprint.Profile{Name: "Built-in Default (Node.js 24.x)"}
|
||
}
|
||
|
||
// --- 缓存管理 ---
|
||
|
||
func (s *TLSFingerprintProfileService) refreshLocalCache(ctx context.Context) error {
|
||
if s.cache != nil {
|
||
if profiles, ok := s.cache.Get(ctx); ok {
|
||
s.setLocalCache(profiles)
|
||
return nil
|
||
}
|
||
}
|
||
return s.reloadFromDB(ctx)
|
||
}
|
||
|
||
func (s *TLSFingerprintProfileService) reloadFromDB(ctx context.Context) error {
|
||
profiles, err := s.repo.List(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if s.cache != nil {
|
||
if err := s.cache.Set(ctx, profiles); err != nil {
|
||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to set cache: %v", err)
|
||
}
|
||
}
|
||
|
||
s.setLocalCache(profiles)
|
||
return nil
|
||
}
|
||
|
||
func (s *TLSFingerprintProfileService) setLocalCache(profiles []*model.TLSFingerprintProfile) {
|
||
m := make(map[int64]*model.TLSFingerprintProfile, len(profiles))
|
||
for _, p := range profiles {
|
||
m[p.ID] = p
|
||
}
|
||
|
||
s.localMu.Lock()
|
||
s.localCache = m
|
||
s.localMu.Unlock()
|
||
}
|
||
|
||
func (s *TLSFingerprintProfileService) newCacheRefreshContext() (context.Context, context.CancelFunc) {
|
||
return context.WithTimeout(context.Background(), 3*time.Second)
|
||
}
|
||
|
||
func (s *TLSFingerprintProfileService) invalidateAndNotify(ctx context.Context) {
|
||
if s.cache != nil {
|
||
if err := s.cache.Invalidate(ctx); err != nil {
|
||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to invalidate cache: %v", err)
|
||
}
|
||
}
|
||
|
||
if err := s.reloadFromDB(ctx); err != nil {
|
||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to refresh local cache: %v", err)
|
||
s.localMu.Lock()
|
||
s.localCache = make(map[int64]*model.TLSFingerprintProfile)
|
||
s.localMu.Unlock()
|
||
}
|
||
|
||
if s.cache != nil {
|
||
if err := s.cache.NotifyUpdate(ctx); err != nil {
|
||
logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to notify cache update: %v", err)
|
||
}
|
||
}
|
||
}
|