feat: 新增会话ID伪装功能,优化日志系统
- 新增 session_id_masking_enabled 配置,启用后将在15分钟内固定 metadata.user_id 中的 session ID - TLS fingerprint 模块日志从自定义 debugLog 迁移到 slog - main.go 添加 slog 初始化,根据 gin mode 设置日志级别 - 前端创建/编辑账号模态框添加会话ID伪装开关 - 多语言支持(中英文)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -44,7 +45,25 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initLogger configures the default slog handler based on gin.Mode().
|
||||||
|
// In non-release mode, Debug level logs are enabled.
|
||||||
|
func initLogger() {
|
||||||
|
var level slog.Level
|
||||||
|
if gin.Mode() == gin.ReleaseMode {
|
||||||
|
level = slog.LevelInfo
|
||||||
|
} else {
|
||||||
|
level = slog.LevelDebug
|
||||||
|
}
|
||||||
|
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: level,
|
||||||
|
})
|
||||||
|
slog.SetDefault(slog.New(handler))
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Initialize slog logger based on gin mode
|
||||||
|
initLogger()
|
||||||
|
|
||||||
// Parse command line flags
|
// Parse command line flags
|
||||||
setupMode := flag.Bool("setup", false, "Run setup wizard in CLI mode")
|
setupMode := flag.Bool("setup", false, "Run setup wizard in CLI mode")
|
||||||
showVersion := flag.Bool("version", false, "Show version information")
|
showVersion := flag.Bool("version", false, "Show version information")
|
||||||
|
|||||||
@@ -166,6 +166,11 @@ func AccountFromServiceShallow(a *service.Account) *Account {
|
|||||||
enabled := true
|
enabled := true
|
||||||
out.EnableTLSFingerprint = &enabled
|
out.EnableTLSFingerprint = &enabled
|
||||||
}
|
}
|
||||||
|
// 会话ID伪装开关
|
||||||
|
if a.IsSessionIDMaskingEnabled() {
|
||||||
|
enabled := true
|
||||||
|
out.EnableSessionIDMasking = &enabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ type Account struct {
|
|||||||
// 从 extra 字段提取,方便前端显示和编辑
|
// 从 extra 字段提取,方便前端显示和编辑
|
||||||
EnableTLSFingerprint *bool `json:"enable_tls_fingerprint,omitempty"`
|
EnableTLSFingerprint *bool `json:"enable_tls_fingerprint,omitempty"`
|
||||||
|
|
||||||
|
// 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||||
|
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
|
||||||
|
// 从 extra 字段提取,方便前端显示和编辑
|
||||||
|
EnableSessionIDMasking *bool `json:"session_id_masking_enabled,omitempty"`
|
||||||
|
|
||||||
Proxy *Proxy `json:"proxy,omitempty"`
|
Proxy *Proxy `json:"proxy,omitempty"`
|
||||||
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
|
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
|
||||||
|
|
||||||
|
|||||||
@@ -7,23 +7,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
utls "github.com/refraction-networking/utls"
|
utls "github.com/refraction-networking/utls"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// debugLog prints log only in non-release mode.
|
|
||||||
func debugLog(format string, v ...any) {
|
|
||||||
if gin.Mode() != gin.ReleaseMode {
|
|
||||||
log.Printf(format, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profile contains TLS fingerprint configuration.
|
// Profile contains TLS fingerprint configuration.
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
Name string // Profile name for identification
|
Name string // Profile name for identification
|
||||||
@@ -229,7 +221,7 @@ func NewSOCKS5ProxyDialer(profile *Profile, proxyURL *url.URL) *SOCKS5ProxyDiale
|
|||||||
// DialTLSContext establishes a TLS connection through SOCKS5 proxy with the configured fingerprint.
|
// DialTLSContext establishes a TLS connection through SOCKS5 proxy with the configured fingerprint.
|
||||||
// Flow: SOCKS5 CONNECT to target -> TLS handshake with utls on the tunnel
|
// Flow: SOCKS5 CONNECT to target -> TLS handshake with utls on the tunnel
|
||||||
func (d *SOCKS5ProxyDialer) DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
func (d *SOCKS5ProxyDialer) DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
debugLog("[TLS Fingerprint SOCKS5] Connecting through proxy %s for target %s", d.proxyURL.Host, addr)
|
slog.Debug("tls_fingerprint_socks5_connecting", "proxy", d.proxyURL.Host, "target", addr)
|
||||||
|
|
||||||
// Step 1: Create SOCKS5 dialer
|
// Step 1: Create SOCKS5 dialer
|
||||||
var auth *proxy.Auth
|
var auth *proxy.Auth
|
||||||
@@ -250,33 +242,37 @@ func (d *SOCKS5ProxyDialer) DialTLSContext(ctx context.Context, network, addr st
|
|||||||
|
|
||||||
socksDialer, err := proxy.SOCKS5("tcp", proxyAddr, auth, proxy.Direct)
|
socksDialer, err := proxy.SOCKS5("tcp", proxyAddr, auth, proxy.Direct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLog("[TLS Fingerprint SOCKS5] Failed to create SOCKS5 dialer: %v", err)
|
slog.Debug("tls_fingerprint_socks5_dialer_failed", "error", err)
|
||||||
return nil, fmt.Errorf("create SOCKS5 dialer: %w", err)
|
return nil, fmt.Errorf("create SOCKS5 dialer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Establish SOCKS5 tunnel to target
|
// Step 2: Establish SOCKS5 tunnel to target
|
||||||
debugLog("[TLS Fingerprint SOCKS5] Establishing SOCKS5 tunnel to %s", addr)
|
slog.Debug("tls_fingerprint_socks5_establishing_tunnel", "target", addr)
|
||||||
conn, err := socksDialer.Dial("tcp", addr)
|
conn, err := socksDialer.Dial("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLog("[TLS Fingerprint SOCKS5] Failed to connect through SOCKS5: %v", err)
|
slog.Debug("tls_fingerprint_socks5_connect_failed", "error", err)
|
||||||
return nil, fmt.Errorf("SOCKS5 connect: %w", err)
|
return nil, fmt.Errorf("SOCKS5 connect: %w", err)
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint SOCKS5] SOCKS5 tunnel established")
|
slog.Debug("tls_fingerprint_socks5_tunnel_established")
|
||||||
|
|
||||||
// Step 3: Perform TLS handshake on the tunnel with utls fingerprint
|
// Step 3: Perform TLS handshake on the tunnel with utls fingerprint
|
||||||
host, _, err := net.SplitHostPort(addr)
|
host, _, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = addr
|
host = addr
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint SOCKS5] Starting TLS handshake to %s", host)
|
slog.Debug("tls_fingerprint_socks5_starting_handshake", "host", host)
|
||||||
|
|
||||||
// Build ClientHello specification from profile (Node.js/Claude CLI fingerprint)
|
// Build ClientHello specification from profile (Node.js/Claude CLI fingerprint)
|
||||||
spec := buildClientHelloSpecFromProfile(d.profile)
|
spec := buildClientHelloSpecFromProfile(d.profile)
|
||||||
debugLog("[TLS Fingerprint SOCKS5] ClientHello spec: CipherSuites=%d, Extensions=%d, CompressionMethods=%v, TLSVersMax=0x%04x, TLSVersMin=0x%04x",
|
slog.Debug("tls_fingerprint_socks5_clienthello_spec",
|
||||||
len(spec.CipherSuites), len(spec.Extensions), spec.CompressionMethods, spec.TLSVersMax, spec.TLSVersMin)
|
"cipher_suites", len(spec.CipherSuites),
|
||||||
|
"extensions", len(spec.Extensions),
|
||||||
|
"compression_methods", spec.CompressionMethods,
|
||||||
|
"tls_vers_max", fmt.Sprintf("0x%04x", spec.TLSVersMax),
|
||||||
|
"tls_vers_min", fmt.Sprintf("0x%04x", spec.TLSVersMin))
|
||||||
|
|
||||||
if d.profile != nil {
|
if d.profile != nil {
|
||||||
debugLog("[TLS Fingerprint SOCKS5] Using profile: %s, GREASE: %v", d.profile.Name, d.profile.EnableGREASE)
|
slog.Debug("tls_fingerprint_socks5_using_profile", "name", d.profile.Name, "grease", d.profile.EnableGREASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create uTLS connection on the tunnel
|
// Create uTLS connection on the tunnel
|
||||||
@@ -285,20 +281,22 @@ func (d *SOCKS5ProxyDialer) DialTLSContext(ctx context.Context, network, addr st
|
|||||||
}, utls.HelloCustom)
|
}, utls.HelloCustom)
|
||||||
|
|
||||||
if err := tlsConn.ApplyPreset(spec); err != nil {
|
if err := tlsConn.ApplyPreset(spec); err != nil {
|
||||||
debugLog("[TLS Fingerprint SOCKS5] ApplyPreset failed: %v", err)
|
slog.Debug("tls_fingerprint_socks5_apply_preset_failed", "error", err)
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, fmt.Errorf("apply TLS preset: %w", err)
|
return nil, fmt.Errorf("apply TLS preset: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
debugLog("[TLS Fingerprint SOCKS5] Handshake FAILED: %v", err)
|
slog.Debug("tls_fingerprint_socks5_handshake_failed", "error", err)
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
state := tlsConn.ConnectionState()
|
state := tlsConn.ConnectionState()
|
||||||
debugLog("[TLS Fingerprint SOCKS5] Handshake SUCCESS - Version: 0x%04x, CipherSuite: 0x%04x, ALPN: %s",
|
slog.Debug("tls_fingerprint_socks5_handshake_success",
|
||||||
state.Version, state.CipherSuite, state.NegotiatedProtocol)
|
"version", fmt.Sprintf("0x%04x", state.Version),
|
||||||
|
"cipher_suite", fmt.Sprintf("0x%04x", state.CipherSuite),
|
||||||
|
"alpn", state.NegotiatedProtocol)
|
||||||
|
|
||||||
return tlsConn, nil
|
return tlsConn, nil
|
||||||
}
|
}
|
||||||
@@ -306,7 +304,7 @@ func (d *SOCKS5ProxyDialer) DialTLSContext(ctx context.Context, network, addr st
|
|||||||
// DialTLSContext establishes a TLS connection through HTTP proxy with the configured fingerprint.
|
// DialTLSContext establishes a TLS connection through HTTP proxy with the configured fingerprint.
|
||||||
// Flow: TCP connect to proxy -> CONNECT tunnel -> TLS handshake with utls
|
// Flow: TCP connect to proxy -> CONNECT tunnel -> TLS handshake with utls
|
||||||
func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Connecting to proxy %s for target %s", d.proxyURL.Host, addr)
|
slog.Debug("tls_fingerprint_http_proxy_connecting", "proxy", d.proxyURL.Host, "target", addr)
|
||||||
|
|
||||||
// Step 1: TCP connect to proxy server
|
// Step 1: TCP connect to proxy server
|
||||||
var proxyAddr string
|
var proxyAddr string
|
||||||
@@ -324,10 +322,10 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri
|
|||||||
dialer := &net.Dialer{}
|
dialer := &net.Dialer{}
|
||||||
conn, err := dialer.DialContext(ctx, "tcp", proxyAddr)
|
conn, err := dialer.DialContext(ctx, "tcp", proxyAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Failed to connect to proxy: %v", err)
|
slog.Debug("tls_fingerprint_http_proxy_connect_failed", "error", err)
|
||||||
return nil, fmt.Errorf("connect to proxy: %w", err)
|
return nil, fmt.Errorf("connect to proxy: %w", err)
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Connected to proxy %s", proxyAddr)
|
slog.Debug("tls_fingerprint_http_proxy_connected", "proxy_addr", proxyAddr)
|
||||||
|
|
||||||
// Step 2: Send CONNECT request to establish tunnel
|
// Step 2: Send CONNECT request to establish tunnel
|
||||||
req := &http.Request{
|
req := &http.Request{
|
||||||
@@ -345,10 +343,10 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri
|
|||||||
req.Header.Set("Proxy-Authorization", "Basic "+auth)
|
req.Header.Set("Proxy-Authorization", "Basic "+auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Sending CONNECT request for %s", addr)
|
slog.Debug("tls_fingerprint_http_proxy_sending_connect", "target", addr)
|
||||||
if err := req.Write(conn); err != nil {
|
if err := req.Write(conn); err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Failed to write CONNECT request: %v", err)
|
slog.Debug("tls_fingerprint_http_proxy_write_failed", "error", err)
|
||||||
return nil, fmt.Errorf("write CONNECT request: %w", err)
|
return nil, fmt.Errorf("write CONNECT request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,32 +355,33 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri
|
|||||||
resp, err := http.ReadResponse(br, req)
|
resp, err := http.ReadResponse(br, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Failed to read CONNECT response: %v", err)
|
slog.Debug("tls_fingerprint_http_proxy_read_response_failed", "error", err)
|
||||||
return nil, fmt.Errorf("read CONNECT response: %w", err)
|
return nil, fmt.Errorf("read CONNECT response: %w", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] CONNECT failed with status: %d %s", resp.StatusCode, resp.Status)
|
slog.Debug("tls_fingerprint_http_proxy_connect_failed_status", "status_code", resp.StatusCode, "status", resp.Status)
|
||||||
return nil, fmt.Errorf("proxy CONNECT failed: %s", resp.Status)
|
return nil, fmt.Errorf("proxy CONNECT failed: %s", resp.Status)
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] CONNECT tunnel established")
|
slog.Debug("tls_fingerprint_http_proxy_tunnel_established")
|
||||||
|
|
||||||
// Step 4: Perform TLS handshake on the tunnel with utls fingerprint
|
// Step 4: Perform TLS handshake on the tunnel with utls fingerprint
|
||||||
host, _, err := net.SplitHostPort(addr)
|
host, _, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = addr
|
host = addr
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Starting TLS handshake to %s", host)
|
slog.Debug("tls_fingerprint_http_proxy_starting_handshake", "host", host)
|
||||||
|
|
||||||
// Build ClientHello specification (reuse the shared method)
|
// Build ClientHello specification (reuse the shared method)
|
||||||
spec := buildClientHelloSpecFromProfile(d.profile)
|
spec := buildClientHelloSpecFromProfile(d.profile)
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] ClientHello spec built with %d cipher suites, %d extensions",
|
slog.Debug("tls_fingerprint_http_proxy_clienthello_spec",
|
||||||
len(spec.CipherSuites), len(spec.Extensions))
|
"cipher_suites", len(spec.CipherSuites),
|
||||||
|
"extensions", len(spec.Extensions))
|
||||||
|
|
||||||
if d.profile != nil {
|
if d.profile != nil {
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Using profile: %s, GREASE: %v", d.profile.Name, d.profile.EnableGREASE)
|
slog.Debug("tls_fingerprint_http_proxy_using_profile", "name", d.profile.Name, "grease", d.profile.EnableGREASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create uTLS connection on the tunnel
|
// Create uTLS connection on the tunnel
|
||||||
@@ -392,20 +391,22 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri
|
|||||||
}, utls.HelloCustom)
|
}, utls.HelloCustom)
|
||||||
|
|
||||||
if err := tlsConn.ApplyPreset(spec); err != nil {
|
if err := tlsConn.ApplyPreset(spec); err != nil {
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] ApplyPreset failed: %v", err)
|
slog.Debug("tls_fingerprint_http_proxy_apply_preset_failed", "error", err)
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, fmt.Errorf("apply TLS preset: %w", err)
|
return nil, fmt.Errorf("apply TLS preset: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Handshake FAILED: %v", err)
|
slog.Debug("tls_fingerprint_http_proxy_handshake_failed", "error", err)
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
state := tlsConn.ConnectionState()
|
state := tlsConn.ConnectionState()
|
||||||
debugLog("[TLS Fingerprint HTTPProxy] Handshake SUCCESS - Version: 0x%04x, CipherSuite: 0x%04x, ALPN: %s",
|
slog.Debug("tls_fingerprint_http_proxy_handshake_success",
|
||||||
state.Version, state.CipherSuite, state.NegotiatedProtocol)
|
"version", fmt.Sprintf("0x%04x", state.Version),
|
||||||
|
"cipher_suite", fmt.Sprintf("0x%04x", state.CipherSuite),
|
||||||
|
"alpn", state.NegotiatedProtocol)
|
||||||
|
|
||||||
return tlsConn, nil
|
return tlsConn, nil
|
||||||
}
|
}
|
||||||
@@ -414,31 +415,32 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri
|
|||||||
// This method is designed to be used as http.Transport.DialTLSContext.
|
// This method is designed to be used as http.Transport.DialTLSContext.
|
||||||
func (d *Dialer) DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
func (d *Dialer) DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
// Establish TCP connection using base dialer (supports proxy)
|
// Establish TCP connection using base dialer (supports proxy)
|
||||||
debugLog("[TLS Fingerprint] Dialing TCP to %s", addr)
|
slog.Debug("tls_fingerprint_dialing_tcp", "addr", addr)
|
||||||
conn, err := d.baseDialer(ctx, network, addr)
|
conn, err := d.baseDialer(ctx, network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLog("[TLS Fingerprint] TCP dial failed: %v", err)
|
slog.Debug("tls_fingerprint_tcp_dial_failed", "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint] TCP connected to %s", addr)
|
slog.Debug("tls_fingerprint_tcp_connected", "addr", addr)
|
||||||
|
|
||||||
// Extract hostname for SNI
|
// Extract hostname for SNI
|
||||||
host, _, err := net.SplitHostPort(addr)
|
host, _, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = addr
|
host = addr
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint] SNI hostname: %s", host)
|
slog.Debug("tls_fingerprint_sni_hostname", "host", host)
|
||||||
|
|
||||||
// Build ClientHello specification
|
// Build ClientHello specification
|
||||||
spec := d.buildClientHelloSpec()
|
spec := d.buildClientHelloSpec()
|
||||||
debugLog("[TLS Fingerprint] ClientHello spec built with %d cipher suites, %d extensions",
|
slog.Debug("tls_fingerprint_clienthello_spec",
|
||||||
len(spec.CipherSuites), len(spec.Extensions))
|
"cipher_suites", len(spec.CipherSuites),
|
||||||
|
"extensions", len(spec.Extensions))
|
||||||
|
|
||||||
// Log profile info
|
// Log profile info
|
||||||
if d.profile != nil {
|
if d.profile != nil {
|
||||||
debugLog("[TLS Fingerprint] Using profile: %s, GREASE: %v", d.profile.Name, d.profile.EnableGREASE)
|
slog.Debug("tls_fingerprint_using_profile", "name", d.profile.Name, "grease", d.profile.EnableGREASE)
|
||||||
} else {
|
} else {
|
||||||
debugLog("[TLS Fingerprint] Using default profile (no custom config)")
|
slog.Debug("tls_fingerprint_using_default_profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create uTLS connection
|
// Create uTLS connection
|
||||||
@@ -449,26 +451,28 @@ func (d *Dialer) DialTLSContext(ctx context.Context, network, addr string) (net.
|
|||||||
|
|
||||||
// Apply fingerprint
|
// Apply fingerprint
|
||||||
if err := tlsConn.ApplyPreset(spec); err != nil {
|
if err := tlsConn.ApplyPreset(spec); err != nil {
|
||||||
debugLog("[TLS Fingerprint] ApplyPreset failed: %v", err)
|
slog.Debug("tls_fingerprint_apply_preset_failed", "error", err)
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint] Preset applied, starting handshake...")
|
slog.Debug("tls_fingerprint_preset_applied")
|
||||||
|
|
||||||
// Perform TLS handshake
|
// Perform TLS handshake
|
||||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
debugLog("[TLS Fingerprint] Handshake FAILED: %v", err)
|
slog.Debug("tls_fingerprint_handshake_failed",
|
||||||
// Log more details about the connection state
|
"error", err,
|
||||||
debugLog("[TLS Fingerprint] Connection state - Local: %v, Remote: %v",
|
"local_addr", conn.LocalAddr(),
|
||||||
conn.LocalAddr(), conn.RemoteAddr())
|
"remote_addr", conn.RemoteAddr())
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log successful handshake details
|
// Log successful handshake details
|
||||||
state := tlsConn.ConnectionState()
|
state := tlsConn.ConnectionState()
|
||||||
debugLog("[TLS Fingerprint] Handshake SUCCESS - Version: 0x%04x, CipherSuite: 0x%04x, ALPN: %s",
|
slog.Debug("tls_fingerprint_handshake_success",
|
||||||
state.Version, state.CipherSuite, state.NegotiatedProtocol)
|
"version", fmt.Sprintf("0x%04x", state.Version),
|
||||||
|
"cipher_suite", fmt.Sprintf("0x%04x", state.CipherSuite),
|
||||||
|
"alpn", state.NegotiatedProtocol)
|
||||||
|
|
||||||
return tlsConn, nil
|
return tlsConn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package tlsfingerprint
|
package tlsfingerprint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ func NewRegistryFromConfig(cfg *config.TLSFingerprintConfig) *Registry {
|
|||||||
r := NewRegistry()
|
r := NewRegistry()
|
||||||
|
|
||||||
if cfg == nil || !cfg.Enabled {
|
if cfg == nil || !cfg.Enabled {
|
||||||
debugLog("[TLS Registry] TLS fingerprint disabled or no config, using default profile only")
|
slog.Debug("tls_registry_disabled", "reason", "disabled or no config")
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,10 +57,10 @@ func NewRegistryFromConfig(cfg *config.TLSFingerprintConfig) *Registry {
|
|||||||
|
|
||||||
// If the profile has empty values, they will use defaults in dialer
|
// If the profile has empty values, they will use defaults in dialer
|
||||||
r.RegisterProfile(name, profile)
|
r.RegisterProfile(name, profile)
|
||||||
debugLog("[TLS Registry] Loaded custom profile: %s (%s)", name, profileCfg.Name)
|
slog.Debug("tls_registry_loaded_profile", "key", name, "name", profileCfg.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog("[TLS Registry] Initialized with %d profiles: %v", len(r.profileNames), r.profileNames)
|
slog.Debug("tls_registry_initialized", "profile_count", len(r.profileNames), "profiles", r.profileNames)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -18,16 +18,8 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
|
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// debugLog prints log only in non-release mode.
|
|
||||||
func debugLog(format string, v ...any) {
|
|
||||||
if gin.Mode() != gin.ReleaseMode {
|
|
||||||
log.Printf(format, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认配置常量
|
// 默认配置常量
|
||||||
// 这些值在配置文件未指定时作为回退默认值使用
|
// 这些值在配置文件未指定时作为回退默认值使用
|
||||||
const (
|
const (
|
||||||
@@ -189,7 +181,7 @@ func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, acco
|
|||||||
if proxyURL != "" {
|
if proxyURL != "" {
|
||||||
proxyInfo = proxyURL
|
proxyInfo = proxyURL
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint] Account %d: TLS fingerprint ENABLED, target=%s, proxy=%s", accountID, targetHost, proxyInfo)
|
slog.Debug("tls_fingerprint_enabled", "account_id", accountID, "target", targetHost, "proxy", proxyInfo)
|
||||||
|
|
||||||
if err := s.validateRequestHost(req); err != nil {
|
if err := s.validateRequestHost(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -200,16 +192,16 @@ func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, acco
|
|||||||
profile := registry.GetProfileByAccountID(accountID)
|
profile := registry.GetProfileByAccountID(accountID)
|
||||||
if profile == nil {
|
if profile == nil {
|
||||||
// 如果获取不到 profile,回退到普通请求
|
// 如果获取不到 profile,回退到普通请求
|
||||||
debugLog("[TLS Fingerprint] Account %d: WARNING - no profile found, falling back to standard request", accountID)
|
slog.Debug("tls_fingerprint_no_profile", "account_id", accountID, "fallback", "standard_request")
|
||||||
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog("[TLS Fingerprint] Account %d: Using profile '%s' (GREASE=%v)", accountID, profile.Name, profile.EnableGREASE)
|
slog.Debug("tls_fingerprint_using_profile", "account_id", accountID, "profile", profile.Name, "grease", profile.EnableGREASE)
|
||||||
|
|
||||||
// 获取或创建带 TLS 指纹的客户端
|
// 获取或创建带 TLS 指纹的客户端
|
||||||
entry, err := s.acquireClientWithTLS(proxyURL, accountID, accountConcurrency, profile)
|
entry, err := s.acquireClientWithTLS(proxyURL, accountID, accountConcurrency, profile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLog("[TLS Fingerprint] Account %d: Failed to acquire TLS client: %v", accountID, err)
|
slog.Debug("tls_fingerprint_acquire_client_failed", "account_id", accountID, "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,11 +211,11 @@ func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, acco
|
|||||||
// 请求失败,立即减少计数
|
// 请求失败,立即减少计数
|
||||||
atomic.AddInt64(&entry.inFlight, -1)
|
atomic.AddInt64(&entry.inFlight, -1)
|
||||||
atomic.StoreInt64(&entry.lastUsed, time.Now().UnixNano())
|
atomic.StoreInt64(&entry.lastUsed, time.Now().UnixNano())
|
||||||
debugLog("[TLS Fingerprint] Account %d: Request FAILED: %v", accountID, err)
|
slog.Debug("tls_fingerprint_request_failed", "account_id", accountID, "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog("[TLS Fingerprint] Account %d: Request SUCCESS, status=%d", accountID, resp.StatusCode)
|
slog.Debug("tls_fingerprint_request_success", "account_id", accountID, "status", resp.StatusCode)
|
||||||
|
|
||||||
// 包装响应体,在关闭时自动减少计数并更新时间戳
|
// 包装响应体,在关闭时自动减少计数并更新时间戳
|
||||||
resp.Body = wrapTrackedBody(resp.Body, func() {
|
resp.Body = wrapTrackedBody(resp.Body, func() {
|
||||||
@@ -259,7 +251,7 @@ func (s *httpUpstreamService) getClientEntryWithTLS(proxyURL string, accountID i
|
|||||||
atomic.AddInt64(&entry.inFlight, 1)
|
atomic.AddInt64(&entry.inFlight, 1)
|
||||||
}
|
}
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
debugLog("[TLS Fingerprint] Account %d: Reusing existing TLS client (cacheKey=%s)", accountID, cacheKey)
|
slog.Debug("tls_fingerprint_reusing_client", "account_id", accountID, "cache_key", cacheKey)
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
@@ -273,11 +265,14 @@ func (s *httpUpstreamService) getClientEntryWithTLS(proxyURL string, accountID i
|
|||||||
atomic.AddInt64(&entry.inFlight, 1)
|
atomic.AddInt64(&entry.inFlight, 1)
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
debugLog("[TLS Fingerprint] Account %d: Reusing existing TLS client (cacheKey=%s)", accountID, cacheKey)
|
slog.Debug("tls_fingerprint_reusing_client", "account_id", accountID, "cache_key", cacheKey)
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
debugLog("[TLS Fingerprint] Account %d: Evicting stale TLS client (cacheKey=%s, proxyChanged=%v, poolChanged=%v)",
|
slog.Debug("tls_fingerprint_evicting_stale_client",
|
||||||
accountID, cacheKey, entry.proxyKey != proxyKey, entry.poolKey != poolKey)
|
"account_id", accountID,
|
||||||
|
"cache_key", cacheKey,
|
||||||
|
"proxy_changed", entry.proxyKey != proxyKey,
|
||||||
|
"pool_changed", entry.poolKey != poolKey)
|
||||||
s.removeClientLocked(cacheKey, entry)
|
s.removeClientLocked(cacheKey, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,8 +288,7 @@ func (s *httpUpstreamService) getClientEntryWithTLS(proxyURL string, accountID i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建带 TLS 指纹的 Transport
|
// 创建带 TLS 指纹的 Transport
|
||||||
debugLog("[TLS Fingerprint] Account %d: Creating NEW TLS fingerprint client (cacheKey=%s, proxy=%s)",
|
slog.Debug("tls_fingerprint_creating_new_client", "account_id", accountID, "cache_key", cacheKey, "proxy", proxyKey)
|
||||||
accountID, cacheKey, proxyKey)
|
|
||||||
settings := s.resolvePoolSettings(isolation, accountConcurrency)
|
settings := s.resolvePoolSettings(isolation, accountConcurrency)
|
||||||
transport, err := buildUpstreamTransportWithTLSFingerprint(settings, parsedProxy, profile)
|
transport, err := buildUpstreamTransportWithTLSFingerprint(settings, parsedProxy, profile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -822,7 +816,7 @@ func buildUpstreamTransportWithTLSFingerprint(settings poolSettings, proxyURL *u
|
|||||||
// 根据代理类型选择合适的 TLS 指纹 Dialer
|
// 根据代理类型选择合适的 TLS 指纹 Dialer
|
||||||
if proxyURL == nil {
|
if proxyURL == nil {
|
||||||
// 直连:使用 TLSFingerprintDialer
|
// 直连:使用 TLSFingerprintDialer
|
||||||
debugLog("[TLS Fingerprint Transport] Using DIRECT TLS dialer (no proxy)")
|
slog.Debug("tls_fingerprint_transport_direct")
|
||||||
dialer := tlsfingerprint.NewDialer(profile, nil)
|
dialer := tlsfingerprint.NewDialer(profile, nil)
|
||||||
transport.DialTLSContext = dialer.DialTLSContext
|
transport.DialTLSContext = dialer.DialTLSContext
|
||||||
} else {
|
} else {
|
||||||
@@ -830,17 +824,17 @@ func buildUpstreamTransportWithTLSFingerprint(settings poolSettings, proxyURL *u
|
|||||||
switch scheme {
|
switch scheme {
|
||||||
case "socks5", "socks5h":
|
case "socks5", "socks5h":
|
||||||
// SOCKS5 代理:使用 SOCKS5ProxyDialer
|
// SOCKS5 代理:使用 SOCKS5ProxyDialer
|
||||||
debugLog("[TLS Fingerprint Transport] Using SOCKS5 TLS dialer (proxy=%s)", proxyURL.Host)
|
slog.Debug("tls_fingerprint_transport_socks5", "proxy", proxyURL.Host)
|
||||||
socks5Dialer := tlsfingerprint.NewSOCKS5ProxyDialer(profile, proxyURL)
|
socks5Dialer := tlsfingerprint.NewSOCKS5ProxyDialer(profile, proxyURL)
|
||||||
transport.DialTLSContext = socks5Dialer.DialTLSContext
|
transport.DialTLSContext = socks5Dialer.DialTLSContext
|
||||||
case "http", "https":
|
case "http", "https":
|
||||||
// HTTP/HTTPS 代理:使用 HTTPProxyDialer(CONNECT 隧道)
|
// HTTP/HTTPS 代理:使用 HTTPProxyDialer(CONNECT 隧道)
|
||||||
debugLog("[TLS Fingerprint Transport] Using HTTP CONNECT TLS dialer (proxy=%s)", proxyURL.Host)
|
slog.Debug("tls_fingerprint_transport_http_connect", "proxy", proxyURL.Host)
|
||||||
httpDialer := tlsfingerprint.NewHTTPProxyDialer(profile, proxyURL)
|
httpDialer := tlsfingerprint.NewHTTPProxyDialer(profile, proxyURL)
|
||||||
transport.DialTLSContext = httpDialer.DialTLSContext
|
transport.DialTLSContext = httpDialer.DialTLSContext
|
||||||
default:
|
default:
|
||||||
// 未知代理类型,回退到普通代理配置(无 TLS 指纹)
|
// 未知代理类型,回退到普通代理配置(无 TLS 指纹)
|
||||||
debugLog("[TLS Fingerprint Transport] WARNING: Unknown proxy scheme '%s', falling back to standard proxy (NO TLS fingerprint)", scheme)
|
slog.Debug("tls_fingerprint_transport_unknown_scheme_fallback", "scheme", scheme)
|
||||||
if err := proxyutil.ConfigureTransportProxy(transport, proxyURL); err != nil {
|
if err := proxyutil.ConfigureTransportProxy(transport, proxyURL); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
fingerprintKeyPrefix = "fingerprint:"
|
fingerprintKeyPrefix = "fingerprint:"
|
||||||
fingerprintTTL = 24 * time.Hour
|
fingerprintTTL = 24 * time.Hour
|
||||||
|
maskedSessionKeyPrefix = "masked_session:"
|
||||||
|
maskedSessionTTL = 15 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
// fingerprintKey generates the Redis key for account fingerprint cache.
|
// fingerprintKey generates the Redis key for account fingerprint cache.
|
||||||
@@ -20,6 +22,11 @@ func fingerprintKey(accountID int64) string {
|
|||||||
return fmt.Sprintf("%s%d", fingerprintKeyPrefix, accountID)
|
return fmt.Sprintf("%s%d", fingerprintKeyPrefix, accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maskedSessionKey generates the Redis key for masked session ID cache.
|
||||||
|
func maskedSessionKey(accountID int64) string {
|
||||||
|
return fmt.Sprintf("%s%d", maskedSessionKeyPrefix, accountID)
|
||||||
|
}
|
||||||
|
|
||||||
type identityCache struct {
|
type identityCache struct {
|
||||||
rdb *redis.Client
|
rdb *redis.Client
|
||||||
}
|
}
|
||||||
@@ -49,3 +56,20 @@ func (c *identityCache) SetFingerprint(ctx context.Context, accountID int64, fp
|
|||||||
}
|
}
|
||||||
return c.rdb.Set(ctx, key, val, fingerprintTTL).Err()
|
return c.rdb.Set(ctx, key, val, fingerprintTTL).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *identityCache) GetMaskedSessionID(ctx context.Context, accountID int64) (string, error) {
|
||||||
|
key := maskedSessionKey(accountID)
|
||||||
|
val, err := c.rdb.Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *identityCache) SetMaskedSessionID(ctx context.Context, accountID int64, sessionID string) error {
|
||||||
|
key := maskedSessionKey(accountID)
|
||||||
|
return c.rdb.Set(ctx, key, sessionID, maskedSessionTTL).Err()
|
||||||
|
}
|
||||||
|
|||||||
@@ -595,6 +595,25 @@ func (a *Account) IsTLSFingerprintEnabled() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSessionIDMaskingEnabled 检查是否启用会话ID伪装
|
||||||
|
// 仅适用于 Anthropic OAuth/SetupToken 类型账号
|
||||||
|
// 启用后将在一段时间内(15分钟)固定 metadata.user_id 中的 session ID,
|
||||||
|
// 使上游认为请求来自同一个会话
|
||||||
|
func (a *Account) IsSessionIDMaskingEnabled() bool {
|
||||||
|
if !a.IsAnthropicOAuthOrSetupToken() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a.Extra == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v, ok := a.Extra["session_id_masking_enabled"]; ok {
|
||||||
|
if enabled, ok := v.(bool); ok {
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// GetWindowCostLimit 获取 5h 窗口费用阈值(美元)
|
// GetWindowCostLimit 获取 5h 窗口费用阈值(美元)
|
||||||
// 返回 0 表示未启用
|
// 返回 0 表示未启用
|
||||||
func (a *Account) GetWindowCostLimit() float64 {
|
func (a *Account) GetWindowCostLimit() float64 {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
mathrand "math/rand"
|
mathrand "math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -45,13 +46,6 @@ func (s *GatewayService) debugModelRoutingEnabled() bool {
|
|||||||
return v == "1" || v == "true" || v == "yes" || v == "on"
|
return v == "1" || v == "true" || v == "yes" || v == "on"
|
||||||
}
|
}
|
||||||
|
|
||||||
// debugLog prints log only in non-release mode.
|
|
||||||
func debugLog(format string, v ...any) {
|
|
||||||
if gin.Mode() != gin.ReleaseMode {
|
|
||||||
log.Printf(format, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func shortSessionHash(sessionHash string) string {
|
func shortSessionHash(sessionHash string) string {
|
||||||
if sessionHash == "" {
|
if sessionHash == "" {
|
||||||
return ""
|
return ""
|
||||||
@@ -425,8 +419,11 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
|
|||||||
for id := range excludedIDs {
|
for id := range excludedIDs {
|
||||||
excludedIDsList = append(excludedIDsList, id)
|
excludedIDsList = append(excludedIDsList, id)
|
||||||
}
|
}
|
||||||
debugLog("[AccountScheduling] Starting account selection: groupID=%v model=%s session=%s excludedIDs=%v",
|
slog.Debug("account_scheduling_starting",
|
||||||
derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), excludedIDsList)
|
"group_id", derefGroupID(groupID),
|
||||||
|
"model", requestedModel,
|
||||||
|
"session", shortSessionHash(sessionHash),
|
||||||
|
"excluded_ids", excludedIDsList)
|
||||||
|
|
||||||
cfg := s.schedulingConfig()
|
cfg := s.schedulingConfig()
|
||||||
|
|
||||||
@@ -1105,11 +1102,19 @@ func (s *GatewayService) listSchedulableAccounts(ctx context.Context, groupID *i
|
|||||||
if s.schedulerSnapshot != nil {
|
if s.schedulerSnapshot != nil {
|
||||||
accounts, useMixed, err := s.schedulerSnapshot.ListSchedulableAccounts(ctx, groupID, platform, hasForcePlatform)
|
accounts, useMixed, err := s.schedulerSnapshot.ListSchedulableAccounts(ctx, groupID, platform, hasForcePlatform)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
debugLog("[AccountScheduling] listSchedulableAccounts (snapshot): groupID=%v platform=%s useMixed=%v count=%d",
|
slog.Debug("account_scheduling_list_snapshot",
|
||||||
derefGroupID(groupID), platform, useMixed, len(accounts))
|
"group_id", derefGroupID(groupID),
|
||||||
|
"platform", platform,
|
||||||
|
"use_mixed", useMixed,
|
||||||
|
"count", len(accounts))
|
||||||
for _, acc := range accounts {
|
for _, acc := range accounts {
|
||||||
debugLog("[AccountScheduling] - Account ID=%d Name=%s Platform=%s Type=%s Status=%s TLSFingerprint=%v",
|
slog.Debug("account_scheduling_account_detail",
|
||||||
acc.ID, acc.Name, acc.Platform, acc.Type, acc.Status, acc.IsTLSFingerprintEnabled())
|
"account_id", acc.ID,
|
||||||
|
"name", acc.Name,
|
||||||
|
"platform", acc.Platform,
|
||||||
|
"type", acc.Type,
|
||||||
|
"status", acc.Status,
|
||||||
|
"tls_fingerprint", acc.IsTLSFingerprintEnabled())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return accounts, useMixed, err
|
return accounts, useMixed, err
|
||||||
@@ -1125,7 +1130,10 @@ func (s *GatewayService) listSchedulableAccounts(ctx context.Context, groupID *i
|
|||||||
accounts, err = s.accountRepo.ListSchedulableByPlatforms(ctx, platforms)
|
accounts, err = s.accountRepo.ListSchedulableByPlatforms(ctx, platforms)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLog("[AccountScheduling] listSchedulableAccounts FAILED: groupID=%v platform=%s err=%v", derefGroupID(groupID), platform, err)
|
slog.Debug("account_scheduling_list_failed",
|
||||||
|
"group_id", derefGroupID(groupID),
|
||||||
|
"platform", platform,
|
||||||
|
"error", err)
|
||||||
return nil, useMixed, err
|
return nil, useMixed, err
|
||||||
}
|
}
|
||||||
filtered := make([]Account, 0, len(accounts))
|
filtered := make([]Account, 0, len(accounts))
|
||||||
@@ -1135,11 +1143,19 @@ func (s *GatewayService) listSchedulableAccounts(ctx context.Context, groupID *i
|
|||||||
}
|
}
|
||||||
filtered = append(filtered, acc)
|
filtered = append(filtered, acc)
|
||||||
}
|
}
|
||||||
debugLog("[AccountScheduling] listSchedulableAccounts (mixed): groupID=%v platform=%s rawCount=%d filteredCount=%d",
|
slog.Debug("account_scheduling_list_mixed",
|
||||||
derefGroupID(groupID), platform, len(accounts), len(filtered))
|
"group_id", derefGroupID(groupID),
|
||||||
|
"platform", platform,
|
||||||
|
"raw_count", len(accounts),
|
||||||
|
"filtered_count", len(filtered))
|
||||||
for _, acc := range filtered {
|
for _, acc := range filtered {
|
||||||
debugLog("[AccountScheduling] - Account ID=%d Name=%s Platform=%s Type=%s Status=%s TLSFingerprint=%v",
|
slog.Debug("account_scheduling_account_detail",
|
||||||
acc.ID, acc.Name, acc.Platform, acc.Type, acc.Status, acc.IsTLSFingerprintEnabled())
|
"account_id", acc.ID,
|
||||||
|
"name", acc.Name,
|
||||||
|
"platform", acc.Platform,
|
||||||
|
"type", acc.Type,
|
||||||
|
"status", acc.Status,
|
||||||
|
"tls_fingerprint", acc.IsTLSFingerprintEnabled())
|
||||||
}
|
}
|
||||||
return filtered, useMixed, nil
|
return filtered, useMixed, nil
|
||||||
}
|
}
|
||||||
@@ -1155,14 +1171,24 @@ func (s *GatewayService) listSchedulableAccounts(ctx context.Context, groupID *i
|
|||||||
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLog("[AccountScheduling] listSchedulableAccounts FAILED: groupID=%v platform=%s err=%v", derefGroupID(groupID), platform, err)
|
slog.Debug("account_scheduling_list_failed",
|
||||||
|
"group_id", derefGroupID(groupID),
|
||||||
|
"platform", platform,
|
||||||
|
"error", err)
|
||||||
return nil, useMixed, err
|
return nil, useMixed, err
|
||||||
}
|
}
|
||||||
debugLog("[AccountScheduling] listSchedulableAccounts (single): groupID=%v platform=%s count=%d",
|
slog.Debug("account_scheduling_list_single",
|
||||||
derefGroupID(groupID), platform, len(accounts))
|
"group_id", derefGroupID(groupID),
|
||||||
|
"platform", platform,
|
||||||
|
"count", len(accounts))
|
||||||
for _, acc := range accounts {
|
for _, acc := range accounts {
|
||||||
debugLog("[AccountScheduling] - Account ID=%d Name=%s Platform=%s Type=%s Status=%s TLSFingerprint=%v",
|
slog.Debug("account_scheduling_account_detail",
|
||||||
acc.ID, acc.Name, acc.Platform, acc.Type, acc.Status, acc.IsTLSFingerprintEnabled())
|
"account_id", acc.ID,
|
||||||
|
"name", acc.Name,
|
||||||
|
"platform", acc.Platform,
|
||||||
|
"type", acc.Type,
|
||||||
|
"status", acc.Status,
|
||||||
|
"tls_fingerprint", acc.IsTLSFingerprintEnabled())
|
||||||
}
|
}
|
||||||
return accounts, useMixed, nil
|
return accounts, useMixed, nil
|
||||||
}
|
}
|
||||||
@@ -2605,9 +2631,10 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
|
|||||||
fingerprint = fp
|
fingerprint = fp
|
||||||
|
|
||||||
// 2. 重写metadata.user_id(需要指纹中的ClientID和账号的account_uuid)
|
// 2. 重写metadata.user_id(需要指纹中的ClientID和账号的account_uuid)
|
||||||
|
// 如果启用了会话ID伪装,会在重写后替换 session 部分为固定值
|
||||||
accountUUID := account.GetExtraString("account_uuid")
|
accountUUID := account.GetExtraString("account_uuid")
|
||||||
if accountUUID != "" && fp.ClientID != "" {
|
if accountUUID != "" && fp.ClientID != "" {
|
||||||
if newBody, err := s.identityService.RewriteUserID(body, account.ID, accountUUID, fp.ClientID); err == nil && len(newBody) > 0 {
|
if newBody, err := s.identityService.RewriteUserIDWithMasking(ctx, body, account, accountUUID, fp.ClientID); err == nil && len(newBody) > 0 {
|
||||||
body = newBody
|
body = newBody
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3638,12 +3665,13 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OAuth 账号:应用统一指纹和重写 userID
|
// OAuth 账号:应用统一指纹和重写 userID
|
||||||
|
// 如果启用了会话ID伪装,会在重写后替换 session 部分为固定值
|
||||||
if account.IsOAuth() && s.identityService != nil {
|
if account.IsOAuth() && s.identityService != nil {
|
||||||
fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header)
|
fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
accountUUID := account.GetExtraString("account_uuid")
|
accountUUID := account.GetExtraString("account_uuid")
|
||||||
if accountUUID != "" && fp.ClientID != "" {
|
if accountUUID != "" && fp.ClientID != "" {
|
||||||
if newBody, err := s.identityService.RewriteUserID(body, account.ID, accountUUID, fp.ClientID); err == nil && len(newBody) > 0 {
|
if newBody, err := s.identityService.RewriteUserIDWithMasking(ctx, body, account, accountUUID, fp.ClientID); err == nil && len(newBody) > 0 {
|
||||||
body = newBody
|
body = newBody
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,6 +51,13 @@ type Fingerprint struct {
|
|||||||
type IdentityCache interface {
|
type IdentityCache interface {
|
||||||
GetFingerprint(ctx context.Context, accountID int64) (*Fingerprint, error)
|
GetFingerprint(ctx context.Context, accountID int64) (*Fingerprint, error)
|
||||||
SetFingerprint(ctx context.Context, accountID int64, fp *Fingerprint) error
|
SetFingerprint(ctx context.Context, accountID int64, fp *Fingerprint) error
|
||||||
|
// GetMaskedSessionID 获取固定的会话ID(用于会话ID伪装功能)
|
||||||
|
// 返回的 sessionID 是一个 UUID 格式的字符串
|
||||||
|
// 如果不存在或已过期(15分钟无请求),返回空字符串
|
||||||
|
GetMaskedSessionID(ctx context.Context, accountID int64) (string, error)
|
||||||
|
// SetMaskedSessionID 设置固定的会话ID,TTL 为 15 分钟
|
||||||
|
// 每次调用都会刷新 TTL
|
||||||
|
SetMaskedSessionID(ctx context.Context, accountID int64, sessionID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// IdentityService 管理OAuth账号的请求身份指纹
|
// IdentityService 管理OAuth账号的请求身份指纹
|
||||||
@@ -203,6 +212,94 @@ func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUI
|
|||||||
return json.Marshal(reqMap)
|
return json.Marshal(reqMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RewriteUserIDWithMasking 重写body中的metadata.user_id,支持会话ID伪装
|
||||||
|
// 如果账号启用了会话ID伪装(session_id_masking_enabled),
|
||||||
|
// 则在完成常规重写后,将 session 部分替换为固定的伪装ID(15分钟内保持不变)
|
||||||
|
func (s *IdentityService) RewriteUserIDWithMasking(ctx context.Context, body []byte, account *Account, accountUUID, cachedClientID string) ([]byte, error) {
|
||||||
|
// 先执行常规的 RewriteUserID 逻辑
|
||||||
|
newBody, err := s.RewriteUserID(body, account.ID, accountUUID, cachedClientID)
|
||||||
|
if err != nil {
|
||||||
|
return newBody, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否启用会话ID伪装
|
||||||
|
if !account.IsSessionIDMaskingEnabled() {
|
||||||
|
return newBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析重写后的 body,提取 user_id
|
||||||
|
var reqMap map[string]any
|
||||||
|
if err := json.Unmarshal(newBody, &reqMap); err != nil {
|
||||||
|
return newBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, ok := reqMap["metadata"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return newBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, ok := metadata["user_id"].(string)
|
||||||
|
if !ok || userID == "" {
|
||||||
|
return newBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找 _session_ 的位置,替换其后的内容
|
||||||
|
const sessionMarker = "_session_"
|
||||||
|
idx := strings.LastIndex(userID, sessionMarker)
|
||||||
|
if idx == -1 {
|
||||||
|
return newBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取或生成固定的伪装 session ID
|
||||||
|
maskedSessionID, err := s.cache.GetMaskedSessionID(ctx, account.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: failed to get masked session ID for account %d: %v", account.ID, err)
|
||||||
|
return newBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if maskedSessionID == "" {
|
||||||
|
// 首次或已过期,生成新的伪装 session ID
|
||||||
|
maskedSessionID = generateRandomUUID()
|
||||||
|
log.Printf("Generated new masked session ID for account %d: %s", account.ID, maskedSessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新 TTL(每次请求都刷新,保持 15 分钟有效期)
|
||||||
|
if err := s.cache.SetMaskedSessionID(ctx, account.ID, maskedSessionID); err != nil {
|
||||||
|
log.Printf("Warning: failed to set masked session ID for account %d: %v", account.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换 session 部分:保留 _session_ 之前的内容,替换之后的内容
|
||||||
|
newUserID := userID[:idx+len(sessionMarker)] + maskedSessionID
|
||||||
|
|
||||||
|
slog.Debug("session_id_masking_applied",
|
||||||
|
"account_id", account.ID,
|
||||||
|
"before", userID,
|
||||||
|
"after", newUserID,
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata["user_id"] = newUserID
|
||||||
|
reqMap["metadata"] = metadata
|
||||||
|
|
||||||
|
return json.Marshal(reqMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRandomUUID 生成随机 UUID v4 格式字符串
|
||||||
|
func generateRandomUUID() string {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
// fallback: 使用时间戳生成
|
||||||
|
h := sha256.Sum256([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
|
||||||
|
b = h[:16]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 UUID v4 版本和变体位
|
||||||
|
b[6] = (b[6] & 0x0f) | 0x40
|
||||||
|
b[8] = (b[8] & 0x3f) | 0x80
|
||||||
|
|
||||||
|
return fmt.Sprintf("%x-%x-%x-%x-%x",
|
||||||
|
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||||
|
}
|
||||||
|
|
||||||
// generateClientID 生成64位十六进制客户端ID(32字节随机数)
|
// generateClientID 生成64位十六进制客户端ID(32字节随机数)
|
||||||
func generateClientID() string {
|
func generateClientID() string {
|
||||||
b := make([]byte, 32)
|
b := make([]byte, 32)
|
||||||
|
|||||||
@@ -1346,6 +1346,33 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Session ID Masking -->
|
||||||
|
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="input-label mb-0">{{ t('admin.accounts.quotaControl.sessionIdMasking.label') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.quotaControl.sessionIdMasking.hint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="sessionIdMaskingEnabled = !sessionIdMaskingEnabled"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
|
sessionIdMaskingEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
|
sessionIdMaskingEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -1928,6 +1955,7 @@ const sessionLimitEnabled = ref(false)
|
|||||||
const maxSessions = ref<number | null>(null)
|
const maxSessions = ref<number | null>(null)
|
||||||
const sessionIdleTimeout = ref<number | null>(null)
|
const sessionIdleTimeout = ref<number | null>(null)
|
||||||
const tlsFingerprintEnabled = ref(false)
|
const tlsFingerprintEnabled = ref(false)
|
||||||
|
const sessionIdMaskingEnabled = ref(false)
|
||||||
|
|
||||||
// Gemini tier selection (used as fallback when auto-detection is unavailable/fails)
|
// Gemini tier selection (used as fallback when auto-detection is unavailable/fails)
|
||||||
const geminiTierGoogleOne = ref<'google_one_free' | 'google_ai_pro' | 'google_ai_ultra'>('google_one_free')
|
const geminiTierGoogleOne = ref<'google_one_free' | 'google_ai_pro' | 'google_ai_ultra'>('google_one_free')
|
||||||
@@ -2314,6 +2342,7 @@ const resetForm = () => {
|
|||||||
maxSessions.value = null
|
maxSessions.value = null
|
||||||
sessionIdleTimeout.value = null
|
sessionIdleTimeout.value = null
|
||||||
tlsFingerprintEnabled.value = false
|
tlsFingerprintEnabled.value = false
|
||||||
|
sessionIdMaskingEnabled.value = false
|
||||||
tempUnschedEnabled.value = false
|
tempUnschedEnabled.value = false
|
||||||
tempUnschedRules.value = []
|
tempUnschedRules.value = []
|
||||||
geminiOAuthType.value = 'code_assist'
|
geminiOAuthType.value = 'code_assist'
|
||||||
@@ -2602,6 +2631,11 @@ const handleAnthropicExchange = async (authCode: string) => {
|
|||||||
extra.enable_tls_fingerprint = true
|
extra.enable_tls_fingerprint = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add session ID masking settings
|
||||||
|
if (sessionIdMaskingEnabled.value) {
|
||||||
|
extra.session_id_masking_enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
const credentials = {
|
const credentials = {
|
||||||
...tokenInfo,
|
...tokenInfo,
|
||||||
...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {})
|
...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {})
|
||||||
@@ -2690,6 +2724,11 @@ const handleCookieAuth = async (sessionKey: string) => {
|
|||||||
extra.enable_tls_fingerprint = true
|
extra.enable_tls_fingerprint = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add session ID masking settings
|
||||||
|
if (sessionIdMaskingEnabled.value) {
|
||||||
|
extra.session_id_masking_enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
const accountName = keys.length > 1 ? `${form.name} #${i + 1}` : form.name
|
const accountName = keys.length > 1 ? `${form.name} #${i + 1}` : form.name
|
||||||
|
|
||||||
// Merge interceptWarmupRequests into credentials
|
// Merge interceptWarmupRequests into credentials
|
||||||
|
|||||||
@@ -759,6 +759,33 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Session ID Masking -->
|
||||||
|
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="input-label mb-0">{{ t('admin.accounts.quotaControl.sessionIdMasking.label') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.quotaControl.sessionIdMasking.hint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="sessionIdMaskingEnabled = !sessionIdMaskingEnabled"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
|
sessionIdMaskingEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
|
sessionIdMaskingEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
@@ -932,6 +959,7 @@ const sessionLimitEnabled = ref(false)
|
|||||||
const maxSessions = ref<number | null>(null)
|
const maxSessions = ref<number | null>(null)
|
||||||
const sessionIdleTimeout = ref<number | null>(null)
|
const sessionIdleTimeout = ref<number | null>(null)
|
||||||
const tlsFingerprintEnabled = ref(false)
|
const tlsFingerprintEnabled = ref(false)
|
||||||
|
const sessionIdMaskingEnabled = ref(false)
|
||||||
|
|
||||||
// Computed: current preset mappings based on platform
|
// Computed: current preset mappings based on platform
|
||||||
const presetMappings = computed(() => getPresetMappingsByPlatform(props.account?.platform || 'anthropic'))
|
const presetMappings = computed(() => getPresetMappingsByPlatform(props.account?.platform || 'anthropic'))
|
||||||
@@ -1266,6 +1294,7 @@ function loadQuotaControlSettings(account: Account) {
|
|||||||
maxSessions.value = null
|
maxSessions.value = null
|
||||||
sessionIdleTimeout.value = null
|
sessionIdleTimeout.value = null
|
||||||
tlsFingerprintEnabled.value = false
|
tlsFingerprintEnabled.value = false
|
||||||
|
sessionIdMaskingEnabled.value = false
|
||||||
|
|
||||||
// Only applies to Anthropic OAuth/SetupToken accounts
|
// Only applies to Anthropic OAuth/SetupToken accounts
|
||||||
if (account.platform !== 'anthropic' || (account.type !== 'oauth' && account.type !== 'setup-token')) {
|
if (account.platform !== 'anthropic' || (account.type !== 'oauth' && account.type !== 'setup-token')) {
|
||||||
@@ -1289,6 +1318,11 @@ function loadQuotaControlSettings(account: Account) {
|
|||||||
if (account.enable_tls_fingerprint === true) {
|
if (account.enable_tls_fingerprint === true) {
|
||||||
tlsFingerprintEnabled.value = true
|
tlsFingerprintEnabled.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load session ID masking setting
|
||||||
|
if (account.session_id_masking_enabled === true) {
|
||||||
|
sessionIdMaskingEnabled.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTempUnschedKeywords(value: unknown) {
|
function formatTempUnschedKeywords(value: unknown) {
|
||||||
@@ -1448,6 +1482,13 @@ const handleSubmit = async () => {
|
|||||||
delete newExtra.enable_tls_fingerprint
|
delete newExtra.enable_tls_fingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session ID masking setting
|
||||||
|
if (sessionIdMaskingEnabled.value) {
|
||||||
|
newExtra.session_id_masking_enabled = true
|
||||||
|
} else {
|
||||||
|
delete newExtra.session_id_masking_enabled
|
||||||
|
}
|
||||||
|
|
||||||
updatePayload.extra = newExtra
|
updatePayload.extra = newExtra
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1293,6 +1293,10 @@ export default {
|
|||||||
tlsFingerprint: {
|
tlsFingerprint: {
|
||||||
label: 'TLS Fingerprint Simulation',
|
label: 'TLS Fingerprint Simulation',
|
||||||
hint: 'Simulate Node.js/Claude Code client TLS fingerprint'
|
hint: 'Simulate Node.js/Claude Code client TLS fingerprint'
|
||||||
|
},
|
||||||
|
sessionIdMasking: {
|
||||||
|
label: 'Session ID Masking',
|
||||||
|
hint: 'When enabled, fixes the session ID in metadata.user_id for 15 minutes, making upstream think requests come from the same session'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
expired: 'Expired',
|
expired: 'Expired',
|
||||||
|
|||||||
@@ -1425,6 +1425,10 @@ export default {
|
|||||||
tlsFingerprint: {
|
tlsFingerprint: {
|
||||||
label: 'TLS 指纹模拟',
|
label: 'TLS 指纹模拟',
|
||||||
hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹'
|
hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹'
|
||||||
|
},
|
||||||
|
sessionIdMasking: {
|
||||||
|
label: '会话 ID 伪装',
|
||||||
|
hint: '启用后将在 15 分钟内固定 metadata.user_id 中的 session ID,使上游认为请求来自同一会话'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
expired: '已过期',
|
expired: '已过期',
|
||||||
|
|||||||
@@ -483,6 +483,10 @@ export interface Account {
|
|||||||
// TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
// TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||||
enable_tls_fingerprint?: boolean | null
|
enable_tls_fingerprint?: boolean | null
|
||||||
|
|
||||||
|
// 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||||
|
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
|
||||||
|
session_id_masking_enabled?: boolean | null
|
||||||
|
|
||||||
// 运行时状态(仅当启用对应限制时返回)
|
// 运行时状态(仅当启用对应限制时返回)
|
||||||
current_window_cost?: number | null // 当前窗口费用
|
current_window_cost?: number | null // 当前窗口费用
|
||||||
active_sessions?: number | null // 当前活跃会话数
|
active_sessions?: number | null // 当前活跃会话数
|
||||||
|
|||||||
Reference in New Issue
Block a user