From 270b0c1af65b8aa0f070dd5093f0984f7fedf677 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 31 Jan 2026 14:17:23 +0700 Subject: [PATCH] feat(http): add uTLS Chrome fingerprint for Cloudflare bypass - Added uTLS library to mimic Chrome's TLS fingerprint - Uses HTTP/2 for optimal performance with uTLS - Auto-detects Cloudflare challenge and retries with Chrome fingerprint - Helps VPN users bypass Cloudflare TLS fingerprint detection --- go_backend/go.mod | 12 +-- go_backend/go.sum | 24 +++--- go_backend/httputil.go | 176 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 15 deletions(-) diff --git a/go_backend/go.mod b/go_backend/go.mod index 500a85f8..ae08271f 100644 --- a/go_backend/go.mod +++ b/go_backend/go.mod @@ -9,15 +9,17 @@ require ( github.com/go-flac/flacpicture v0.3.0 github.com/go-flac/flacvorbis v0.2.0 github.com/go-flac/go-flac v1.0.0 + github.com/refraction-networking/utls v1.8.2 + golang.org/x/net v0.49.0 ) require ( + github.com/andybalholm/brotli v1.0.6 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect - golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/text v0.3.8 // indirect - golang.org/x/tools v0.40.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect ) diff --git a/go_backend/go.sum b/go_backend/go.sum index 68a7935e..5614dfee 100644 --- a/go_backend/go.sum +++ b/go_backend/go.sum @@ -1,5 +1,7 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 h1:bVp3yUzvSAJzu9GqID+Z96P+eu5TKnIMJSV4QaZMauM= @@ -14,15 +16,17 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyL github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= -golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 h1:Cr6kbEvA6nqvdHynE4CtVKlqpZB9dS1Jva/6IsHA19g= -golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294/go.mod h1:RdZ+3sb4CVgpCFnzv+I4haEpwqFfsfzlLHs3L7ok+e0= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= +github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/go_backend/httputil.go b/go_backend/httputil.go index fcd0377d..c62b2cf5 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -1,6 +1,7 @@ package gobackend import ( + "context" "crypto/tls" "errors" "fmt" @@ -11,8 +12,12 @@ import ( "net/url" "strconv" "strings" + "sync" "syscall" "time" + + utls "github.com/refraction-networking/utls" + "golang.org/x/net/http2" ) // getRandomUserAgent generates a random Windows Chrome User-Agent string @@ -59,6 +64,96 @@ var sharedTransport = &http.Transport{ DisableCompression: true, } +// uTLS transport that mimics Chrome's TLS fingerprint to bypass Cloudflare +// Uses HTTP/2 for optimal performance as uTLS works best with HTTP/2 +type utlsTransport struct { + dialer *net.Dialer + mu sync.Mutex + h2Transports map[string]*http2.Transport +} + +func newUTLSTransport() *utlsTransport { + return &utlsTransport{ + dialer: &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }, + h2Transports: make(map[string]*http2.Transport), + } +} + +func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // For non-HTTPS, use standard transport + if req.URL.Scheme != "https" { + return sharedTransport.RoundTrip(req) + } + + host := req.URL.Hostname() + port := t.getPort(req.URL) + addr := net.JoinHostPort(host, port) + + // Dial TCP connection + conn, err := t.dialer.DialContext(req.Context(), "tcp", addr) + if err != nil { + return nil, err + } + + // Create uTLS connection with Chrome fingerprint (supports HTTP/2 ALPN) + tlsConn := utls.UClient(conn, &utls.Config{ + ServerName: host, + NextProtos: []string{"h2", "http/1.1"}, // Prefer HTTP/2 + }, utls.HelloChrome_Auto) + + // Perform TLS handshake + if err := tlsConn.Handshake(); err != nil { + conn.Close() + return nil, err + } + + // Check if server supports HTTP/2 + negotiatedProto := tlsConn.ConnectionState().NegotiatedProtocol + + if negotiatedProto == "h2" { + // Use HTTP/2 transport + h2Transport := &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { + return tlsConn, nil + }, + AllowHTTP: false, + DisableCompression: false, + } + return h2Transport.RoundTrip(req) + } + + // Fallback to HTTP/1.1 + transport := &http.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return tlsConn, nil + }, + DisableKeepAlives: true, + } + + return transport.RoundTrip(req) +} + +func (t *utlsTransport) getPort(u *url.URL) string { + if u.Port() != "" { + return u.Port() + } + if u.Scheme == "https" { + return "443" + } + return "80" +} + +// Cloudflare bypass client using uTLS Chrome fingerprint +var cloudflareBypassTransport = newUTLSTransport() + +var cloudflareBypassClient = &http.Client{ + Transport: cloudflareBypassTransport, + Timeout: DefaultTimeout, +} + var sharedClient = &http.Client{ Transport: sharedTransport, Timeout: DefaultTimeout, @@ -84,6 +179,12 @@ func GetDownloadClient() *http.Client { return downloadClient } +// GetCloudflareBypassClient returns an HTTP client that mimics Chrome's TLS fingerprint +// Use this when requests are blocked by Cloudflare (common when using VPN) +func GetCloudflareBypassClient() *http.Client { + return cloudflareBypassClient +} + // CloseIdleConnections closes idle connections in the shared transport func CloseIdleConnections() { sharedTransport.CloseIdleConnections() @@ -99,6 +200,81 @@ func DoRequestWithUserAgent(client *http.Client, req *http.Request) (*http.Respo return resp, err } +// DoRequestWithCloudflareBypass attempts request with standard client first, +// then retries with uTLS Chrome fingerprint if Cloudflare blocks it. +// This is useful when using VPN as Cloudflare detects Go's default TLS fingerprint. +func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) { + req.Header.Set("User-Agent", getRandomUserAgent()) + + // Try with standard client first + resp, err := sharedClient.Do(req) + if err == nil { + // Check for Cloudflare challenge page (403 with specific markers) + if resp.StatusCode == 403 || resp.StatusCode == 503 { + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + + if readErr == nil { + bodyStr := strings.ToLower(string(body)) + cloudflareMarkers := []string{ + "cloudflare", "cf-ray", "checking your browser", + "please wait", "ddos protection", "ray id", + "enable javascript", "challenge-platform", + } + + isCloudflare := false + for _, marker := range cloudflareMarkers { + if strings.Contains(bodyStr, marker) { + isCloudflare = true + break + } + } + + if isCloudflare { + LogDebug("HTTP", "Cloudflare detected, retrying with Chrome TLS fingerprint...") + + // Clone request for retry + reqCopy := req.Clone(req.Context()) + reqCopy.Header.Set("User-Agent", getRandomUserAgent()) + + // Retry with uTLS Chrome fingerprint + return cloudflareBypassClient.Do(reqCopy) + } + } + + // Not Cloudflare, return original response (recreate body) + return &http.Response{ + Status: resp.Status, + StatusCode: resp.StatusCode, + Header: resp.Header, + Body: io.NopCloser(strings.NewReader(string(body))), + }, nil + } + return resp, nil + } + + // Check if error might be TLS-related (Cloudflare blocking) + errStr := strings.ToLower(err.Error()) + tlsRelated := strings.Contains(errStr, "tls") || + strings.Contains(errStr, "handshake") || + strings.Contains(errStr, "certificate") || + strings.Contains(errStr, "connection reset") + + if tlsRelated { + LogDebug("HTTP", "TLS error detected, retrying with Chrome TLS fingerprint: %v", err) + + // Clone request for retry + reqCopy := req.Clone(req.Context()) + reqCopy.Header.Set("User-Agent", getRandomUserAgent()) + + // Retry with uTLS Chrome fingerprint + return cloudflareBypassClient.Do(reqCopy) + } + + CheckAndLogISPBlocking(err, req.URL.String(), "HTTP") + return nil, err +} + // RetryConfig holds configuration for retry logic type RetryConfig struct { MaxRetries int