diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 2c1ac5b0..0e5182e0 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -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 @@ -167,7 +172,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() diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index ef9c81f7..02d9c37e 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -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 { // 粘性会话排队配置 diff --git a/backend/internal/pkg/claude/constants.go b/backend/internal/pkg/claude/constants.go index 973bf79f..7fcbeee6 100644 --- a/backend/internal/pkg/claude/constants.go +++ b/backend/internal/pkg/claude/constants.go @@ -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"` diff --git a/backend/internal/service/identity_service.go b/backend/internal/service/identity_service.go index a006cea9..7fc7beb3 100644 --- a/backend/internal/service/identity_service.go +++ b/backend/internal/service/identity_service.go @@ -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) // 根据客户端版本选择输出格式 diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index 738b622f..63df74a1 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -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 # 日志配置