feat: 实例级隔离 — salt + 指纹版本可配置
Some checks failed
CI / test (push) Failing after 3s
CI / golangci-lint (push) Failing after 2s
Security Scan / backend-security (push) Failing after 2s
Security Scan / frontend-security (push) Failing after 2s

- 新增 gateway.instance_salt: 不同 sub2api 实例对相同输入产生不同 hash
  影响 user_id 重写和 session hash,防止跨实例指纹关联
- 新增 gateway.fingerprint_defaults: CLI 版本号/SDK 版本/OS/Arch 可配置
  每个实例可设不同值,与其他 sub2api 部署区分
- constants.go + identity_service.go 支持启动时覆盖默认指纹
- wire_gen.go 启动时读取配置并应用覆盖
This commit is contained in:
win 2026-03-22 03:01:55 +08:00
parent 43506e4f78
commit 73bbbb415c
5 changed files with 106 additions and 4 deletions

View File

@ -12,6 +12,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/handler"
"github.com/Wei-Shaw/sub2api/internal/handler/admin"
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
"github.com/Wei-Shaw/sub2api/internal/repository"
"github.com/Wei-Shaw/sub2api/internal/server"
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
@ -35,6 +36,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
if err != nil {
return nil, err
}
// 应用实例级指纹覆盖(不同 sub2api 实例可设不同的默认版本号)
fpd := configConfig.Gateway.FingerprintDefaults
claude.ApplyFingerprintOverrides(fpd.ClaudeCLIVersion, fpd.StainlessPackageVersion, fpd.StainlessRuntimeVersion, fpd.StainlessOS, fpd.StainlessArch)
service.ApplyDefaultFingerprintOverrides(fpd.ClaudeCLIVersion, fpd.StainlessPackageVersion, fpd.StainlessRuntimeVersion, fpd.StainlessOS, fpd.StainlessArch)
client, err := repository.ProvideEnt(configConfig)
if err != nil {
return nil, err
@ -166,7 +171,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
return nil, err
}
billingService := service.NewBillingService(configConfig, pricingService)
identityService := service.NewIdentityService(identityCache)
identityService := service.NewIdentityServiceWithSalt(identityCache, configConfig.Gateway.InstanceSalt)
deferredService := service.ProvideDeferredService(accountRepository, timingWheelService)
claudeTokenProvider := service.ProvideClaudeTokenProvider(accountRepository, geminiTokenCache, oAuthService, oauthRefreshAPI)
digestSessionStore := service.NewDigestSessionStore()

View File

@ -461,6 +461,18 @@ type GatewayConfig struct {
// 实现天然 JA3/JA4 指纹匹配(无需 uTLS 模拟)
NodeTLSProxy NodeTLSProxyConfig `mapstructure:"node_tls_proxy"`
// InstanceSalt: 实例级隔离盐值
// 用于 user_id 重写和 session hash 的种子混淆,
// 不同 sub2api 实例设置不同的 salt确保相同输入产生不同输出。
// 为空时使用默认行为(无 salt建议生产环境必须配置。
// 生成方法: openssl rand -hex 32
InstanceSalt string `mapstructure:"instance_salt"`
// FingerprintDefaults: 指纹默认值覆盖
// 允许每个实例配置不同的 Claude CLI 版本号,与其他 sub2api 实例区分。
// 为空时使用代码内置默认值。
FingerprintDefaults FingerprintDefaultsConfig `mapstructure:"fingerprint_defaults"`
// UsageRecord: 使用量记录异步队列配置(有界队列 + 固定 worker
UsageRecord GatewayUsageRecordConfig `mapstructure:"usage_record"`
@ -696,6 +708,23 @@ type NodeTLSProxyConfig struct {
ProxyHosts []string `mapstructure:"proxy_hosts"`
}
// FingerprintDefaultsConfig 指纹默认值配置
// 允许每个 sub2api 实例设置不同的默认指纹值,与其他实例区分。
// 所有字段为空时使用代码内置默认值。
type FingerprintDefaultsConfig struct {
// ClaudeCLIVersion: Claude CLI 版本号(如 "2.1.81"
// 最终 User-Agent 为 "claude-cli/{version} (external, cli)"
ClaudeCLIVersion string `mapstructure:"claude_cli_version"`
// StainlessPackageVersion: @anthropic-ai/sdk 版本(如 "0.80.0"
StainlessPackageVersion string `mapstructure:"stainless_package_version"`
// StainlessRuntimeVersion: Node.js 版本(如 "v24.13.0"
StainlessRuntimeVersion string `mapstructure:"stainless_runtime_version"`
// StainlessOS: 操作系统(如 "Linux", "Darwin"
StainlessOS string `mapstructure:"stainless_os"`
// StainlessArch: 架构(如 "arm64", "x64"
StainlessArch string `mapstructure:"stainless_arch"`
}
// GatewaySchedulingConfig accounts scheduling configuration.
type GatewaySchedulingConfig struct {
// 粘性会话排队配置

View File

@ -61,6 +61,30 @@ var DefaultHeaders = map[string]string{
"Anthropic-Dangerous-Direct-Browser-Access": "true",
}
// ApplyFingerprintOverrides 用配置覆盖默认指纹值(每个实例可设不同值)
// cliVersion: Claude CLI 版本(如 "2.1.81"
// pkgVersion: SDK 版本(如 "0.80.0"
// runtimeVersion: Node.js 版本(如 "v24.13.0"
// os_: 操作系统(如 "Linux"
// arch: 架构(如 "arm64"
func ApplyFingerprintOverrides(cliVersion, pkgVersion, runtimeVersion, os_, arch string) {
if cliVersion != "" {
DefaultHeaders["User-Agent"] = "claude-cli/" + cliVersion + " (external, cli)"
}
if pkgVersion != "" {
DefaultHeaders["X-Stainless-Package-Version"] = pkgVersion
}
if runtimeVersion != "" {
DefaultHeaders["X-Stainless-Runtime-Version"] = runtimeVersion
}
if os_ != "" {
DefaultHeaders["X-Stainless-OS"] = os_
}
if arch != "" {
DefaultHeaders["X-Stainless-Arch"] = arch
}
}
// Model 表示一个 Claude 模型
type Model struct {
ID string `json:"id"`

View File

@ -35,6 +35,25 @@ var defaultFingerprint = Fingerprint{
StainlessRuntimeVersion: "v24.13.0",
}
// ApplyDefaultFingerprintOverrides 用配置覆盖 identity_service 的默认指纹
func ApplyDefaultFingerprintOverrides(cliVersion, pkgVersion, runtimeVersion, os_, arch string) {
if cliVersion != "" {
defaultFingerprint.UserAgent = "claude-cli/" + cliVersion + " (external, cli)"
}
if pkgVersion != "" {
defaultFingerprint.StainlessPackageVersion = pkgVersion
}
if runtimeVersion != "" {
defaultFingerprint.StainlessRuntimeVersion = runtimeVersion
}
if os_ != "" {
defaultFingerprint.StainlessOS = os_
}
if arch != "" {
defaultFingerprint.StainlessArch = arch
}
}
// Fingerprint represents account fingerprint data
type Fingerprint struct {
ClientID string
@ -63,7 +82,8 @@ type IdentityCache interface {
// IdentityService 管理OAuth账号的请求身份指纹
type IdentityService struct {
cache IdentityCache
cache IdentityCache
instanceSalt string // 实例级隔离盐值,不同 sub2api 实例产生不同的 hash 输出
}
// NewIdentityService 创建新的IdentityService
@ -71,6 +91,11 @@ func NewIdentityService(cache IdentityCache) *IdentityService {
return &IdentityService{cache: cache}
}
// NewIdentityServiceWithSalt 创建带实例盐值的 IdentityService
func NewIdentityServiceWithSalt(cache IdentityCache, salt string) *IdentityService {
return &IdentityService{cache: cache, instanceSalt: salt}
}
// GetOrCreateFingerprint 获取或创建账号的指纹
// 如果缓存存在检测user-agent版本新版本则更新
// 如果缓存不存在生成随机ClientID并从请求头创建指纹然后缓存
@ -241,8 +266,9 @@ func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUI
sessionTail := parsed.SessionID // 原始session UUID
// 生成新的session hash: SHA256(accountID::sessionTail) -> UUID格式
seed := fmt.Sprintf("%d::%s", accountID, sessionTail)
// 生成新的session hash: SHA256(salt::accountID::sessionTail) -> UUID格式
// instanceSalt 使不同 sub2api 实例对相同输入产生不同的 hash
seed := fmt.Sprintf("%s::%d::%s", s.instanceSalt, accountID, sessionTail)
newSessionHash := generateUUIDFromSeed(seed)
// 根据客户端版本选择输出格式

View File

@ -397,6 +397,24 @@ gateway:
# Upstream target host / 上游目标主机
upstream_host: "api.anthropic.com"
# Instance isolation salt / 实例隔离盐值
# IMPORTANT: Each sub2api deployment MUST set a unique salt to prevent
# cross-instance fingerprint correlation. Generate: openssl rand -hex 32
# 重要:每个 sub2api 实例必须设置唯一的 salt防止不同实例之间的指纹关联。
# 生成方法: openssl rand -hex 32
instance_salt: ""
# Fingerprint defaults override / 指纹默认值覆盖
# Each instance can set different version numbers to differentiate from
# other sub2api deployments. Empty values use built-in defaults.
# 每个实例可设置不同的版本号,与其他 sub2api 部署区分。空值使用内置默认值。
fingerprint_defaults:
# claude_cli_version: "2.1.81"
# stainless_package_version: "0.80.0"
# stainless_runtime_version: "v24.13.0"
# stainless_os: "Linux" # Linux / Darwin
# stainless_arch: "arm64" # arm64 / x64
# =============================================================================
# Logging Configuration
# 日志配置