Jeśli budujesz scrapery lub klientów HTTP w Go i nagle zaczynasz otrzymywać HTTP 403 od Cloudflare czy Akamai — mimo poprawnych nagłówków, ciasteczek i realistycznego User-Agent — problemem prawdopodobnie nie jest warstwa aplikacji, ale odcisk TLS. Naprawa odcisku TLS w Go net/http to kluczowy krok, bo standardowa biblioteka crypto/tls emituje statyczny, nierozpoznawalnie „ludzki" ClientHello, który systemy anti-bot flagują w milisekundach. W tym przewodniku pokazujemy, jak zidentyfikować swój JA3/JA4, podmienić handshake na Chrome za pomocą uTLS i połączyć to z proxy residential ProxyHat, aby zarówno odcisk TLS, jak i reputacja IP wyglądały jak prawdziwy użytkownik.
Naprawa odcisku TLS w Go net/http: dlaczego standardowy klient zostaje zablokowany
Go od wersji 1.0 używa własnej implementacji TLS w pakiecie crypto/tls. W przeciwieństwie do przeglądarek, które regularnie aktualizują swoje ClientHello — dodając rozszerzenia, zmieniając kolejność szyfrów, włączając GREASE — Go utrzymuje stały, przewidywalny wzorzec handshake'u. Wynika to z designu: Go stawia na prostotę i stabilność API, a nie na mimikrę przeglądarkową.
Konsekwencje są konkretne. Cloudflare Bot Management, Akamai Bot Manager, DataDome i PerimeterX (HUMAN) wszystkie wykorzystują TLS fingerprinting jako jedną z pierwszych warstw detekcji. Kiedy Twój klient Go łączy się z ich chronionym serwerem, WAF analizuje ClientHello i porównuje JA3/JA4 z bazą znanych przeglądarek. Jeśli odcisk nie pasuje do Chrome, Firefox czy Safari — dostajesz blokadę, zanim jeszcze wysłano pierwszy bajt HTTP.
Naprawa odcisku TLS w Go net/http zaczyna się od diagnozy. Możesz sprawdzić swój aktualny odcisk, wysyłając żądanie do tls.peet.ws/api/all — darmowej usługi, która zwraca Twój JA3, JA4 i pełną listę rozszerzeń TLS:
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
)
func main() {
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
},
}
resp, err := client.Get("https://tls.peet.ws/api/all")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Odpowiedź zawiera m.in. ja3, ja4, listę ciphers, extensions i supported_groups. Jeśli korzystasz ze standardowego net/http, zobaczysz odcisk, który nie pasuje do żadnej znanej przeglądarki — i to jest dokładnie sygnał, który WAF-y wykorzystują.
Jak działa fingerprinting TLS — JA3 i JA4
JA3 to algorytm fingerprintingu TLS opisany przez Salesforce w 2017 roku. Tworzy hash MD5 z konkatenacji: wersji TLS, listy szyfrów, listy rozszerzeń, listy krzywych eliptycznych (supported_groups) oraz formatów sygnatur (signature_algorithms). Wynikowy hash to identyfikator, który jest stały dla danego klienta — Go 1.21 ma inny JA3 niż Go 1.22, ale w obrębie jednej wersji wszystkie instancje mają ten sam odcisk.
JA4, wprowadzony przez FoxIO w 2024 roku, rozwija koncepcję JA3 o dodatkowe pola i formatowanie. Zamiast jednego hasha, JA4 generuje pięciosegmentowy identyfikator zawierający protokół transportu (TCP/quic), wersję TLS, liczbę rozszerzeń, liczbę szyfrów oraz hash z ich list. JA4 jest bardziej odporny na reordering i daje lepszą granulację. Wiele WAF-ów już migruje z JA3 na JA4 lub używa obu jednocześnie.
Dla inżyniera Go to oznacza, że nawet jeśli zmienisz User-Agent na Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, WAF natychmiast widzi rozbieżność: nagłówek mówi „Chrome", ale JA3 mówi „Go standard library". Ta niespójność to czerwona flaga — silniki anti-bot traktują ją jako silniejszy sygnał botowania niż sam nietypowy odcisk.
Sygnały ClientHello: Go vs Chrome — konkretne różnice
Rozłóżmy ClientHello na czynniki pierwsze. Poniższa tabela porównuje sygnały, które WAF-y sprawdzają najczęściej, między standardowym Go 1.22 a Chrome 120 na Windows:
| Sygnał ClientHello | Go 1.22 (net/http) | Chrome 120 (Windows) |
|---|---|---|
| Wersja TLS | TLS 1.3 (z fallback do 1.2) | TLS 1.3 |
| Liczba szyfrów | ~17 | ~17 (z GREASE) |
| Kolejność szyfrów | Stała, alfabetyczna wg Go | GREASE na pozycji 0, potem AES-GCM |
| GREASE w szyfrach | Brak | Tak (0x0a0a, 0x1a1a, ...) |
| GREASE w rozszerzeniach | Brak | Tak (losowe wartości GREASE) |
| supported_groups | x25519, secp256r1, secp384r1 | GREASE, x25519, secp256r1, secp384r1 |
| signature_algorithms | PKCS1 v1.5, PSS, ECDSA | PSS, ECDSA, PKCS1 (inna kolejność) |
| ALPN | h2, http/1.1 | h2, http/1.1 (z GREASE) |
| key_share | x25519, secp256r1 | GREASE, x25519, secp256r1 |
| Rozszerzenie padding (76) | Brak | Tak (padding do 512 bajtów) |
| encrypted_client_hello (ECH) | Brak | Tak (eksperymentalnie) |
Najbardziej krytyczne różnice to brak GREASE i brak rozszerzenia padding. GREASE (RFC 8701) to mechanizm, w którym przeglądarki celowo wstawia zarezerwowane, bezsensowne wartości w listy szyfrów, grup i rozszerzeń, aby zapobiec ossyfikacji protokołu. Go crypto/tls nie implementuje GREASE — i to jest natychmiast rozpoznawalne. Możesz przeczytać więcej o GREASE w RFC 8701.
Rozszerzenie padding (numer 76) jest równie ważne. Chrome paddinguje ClientHello do 512 bajtów, aby ukryć długość listy rozszerzeń — Go tego nie robi, więc długość ClientHello jest inna niż u przeglądarek, co jest kolejnym sygnałem dla zaawansowanych WAF-ów.
Rozwiązanie: uTLS i modyfikacja handshake'u
refraction-networking/utls to fork crypto/tls, który pozwala na pełną kontrolę nad ClientHello. Zamiast statycznego handshake'u, uTLS oferuje gotowe profile parroting — czyli dokładne repliki ClientHello konkretnych przeglądarek, aktualizowane wraz z ich zmianami.
Kluczowe API to utls.UClient + wybór profilu. Najczęściej używane:
utls.HelloChrome_Auto— automatycznie wybiera najnowszy profil Chromeutls.HelloSafari_16_0— profil Safari 16.0 (przydatny, gdy potrzebujesz ustawić odcisk TLS na Safari)utls.HelloFirefox_105— profil Firefox 105utls.HelloIOS_14— profil iOS Safari
Aby podłączyć uTLS do http.Transport, musisz zaimplementować DialTLSContext. Oto kompletny przykład z HelloChrome_Auto:
package main
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"time"
utls "github.com/refraction-networking/utls"
)
func main() {
transport := &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
rawConn, err := (&net.Dialer{
Timeout: 30 * time.Second,
}).DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
uConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
}, utls.HelloChrome_Auto)
if err := uConn.HandshakeContext(ctx); err != nil {
rawConn.Close()
return nil, err
}
return uConn, nil
},
ForceAttemptHTTP2: true,
}
client := &http.Client{
Transport: transport,
Timeout: 60 * time.Second,
}
resp, err := client.Get("https://tls.peet.ws/api/all")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Po uruchomieniu tego kodu, tls.peet.ws/api/all powinno zwrócić JA3/JA4 pasujący do Chrome. To jest fundament naprawy odcisku TLS w Go — mimikra handshake'u, a nie tylko nagłówków HTTP.
Jeśli potrzebujesz profilu Safari zamiast Chrome, zamień utls.HelloChrome_Auto na utls.HelloSafari_16_0. Wybór profilu powinien być spójny z User-Agentem, który wysyłasz — jeśli UA mówi Safari, odcisk TLS musi mówić to samo.
Alternatywy wyższego poziomu: CycleTLS i azuretls-client
Jeśli nie chcesz ręcznie implementować DialTLSContext, istnieją biblioteki wyższego poziomu, które opakowują uTLS i dają gotowe API:
| Biblioteka | Opis | Poziom abstrakcji | Proxy support |
|---|---|---|---|
| uTLS (bezpośrednio) | Pełna kontrola, własny DialTLSContext | Niski (DIY) | Ręczny |
| CycleTLS | Opakowanie uTLS z prostym API | Średni | HTTP/SOCKS5 wbudowane |
| azuretls-client | Pełny klient HTTP z uTLS, JA3 spoofing, proxy | Wysoki | HTTP/SOCKS5 wbudowane |
CycleTLS (GitHub) to biblioteka, która upraszcza uTLS do API w stylu CycleTLS.Index().Go(). Pozwala ustawiać JA3 string, co jest wygodne, ale mniej elastyczne niż HelloChrome_Auto, które aktualizuje się automatycznie wraz z nowymi wersjami uTLS.
azuretls-client to nowsza opcja, która łączy uTLS z pełnym klientem HTTP, zarządzaniem ciasteczkami i proxy. Jest wygodna, gdy budujesz scraper produkcyjny i nie chcesz martwić się o szczegóły TLS.
Wybór zależy od kontroli, jakiej potrzebujesz. Bezpośredni uTLS daje maksymalną elastyczność — możesz mieszać profile, dostosowywać rozszerzenia, debugować handshake. CycleTLS i azuretls są szybsze wdrożyć, ale ograniczają możliwość dostosowania.
Połączenie uTLS z proxy residential ProxyHat
Mimikra TLS to tylko połowa sukcesu. Nawet z idealnym odciskiem Chrome, jeśli Twoje żądania idą z IP datacenter (AWS, DigitalOcean, Hetzner), WAF-y odrzucą je na podstawie reputacji IP. Cloudflare i Akamai utrzymują bazy IP datacenter i blokują je domyślnie, niezależnie od odcisku TLS.
Rozwiązanie to residential proxy — IP pochodzące od prawdziwych dostawców internetu (ISP), które WAF-y traktują jak zwykłych użytkowników domowych. ProxyHat oferuje sieć proxy residential, którą możesz łatwo zintegrować z klientem uTLS w Go.
Oto kompletny, uruchamialny przykład, który łączy uTLS HelloChrome_Auto z proxy residential ProxyHat, używając geo-targetingu na USA i sesji sticky:
package main
import (
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
utls "github.com/refraction-networking/utls"
)
func main() {
proxyUser := "user-country-US-session-abc123"
proxyPass := "twoje_haslo"
proxyAddr := "gate.proxyhat.com:8080"
transport := &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
// Polacz sie z proxy ProxyHat
rawConn, err := (&net.Dialer{
Timeout: 30 * time.Second,
}).DialContext(ctx, "tcp", proxyAddr)
if err != nil {
return nil, err
}
// Wyslij CONNECT z autoryzacja Basic
auth := base64.StdEncoding.EncodeToString(
[]byte(proxyUser + ":" + proxyPass))
connectReq := fmt.Sprintf(
"CONNECT %s HTTP/1.1\r\nHost: %s\r\nProxy-Authorization: Basic %s\r\n\r\n",
addr, addr, auth)
_, err = rawConn.Write([]byte(connectReq))
if err != nil {
rawConn.Close()
return nil, err
}
// Przeczytaj odpowiedz proxy
buf := make([]byte, 1024)
n, err := rawConn.Read(buf)
if err != nil {
rawConn.Close()
return nil, err
}
if !strings.Contains(string(buf[:n]), "200") {
rawConn.Close()
return nil, fmt.Errorf("proxy CONNECT failed: %s",
string(buf[:n]))
}
// Nakloz uTLS handshake na tunelowane polaczenie
uConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
}, utls.HelloChrome_Auto)
if err := uConn.HandshakeContext(ctx); err != nil {
rawConn.Close()
return nil, err
}
return uConn, nil
},
ForceAttemptHTTP2: true,
TLSClientConfig: &tls.Config{},
}
client := &http.Client{
Transport: transport,
Timeout: 60 * time.Second,
}
req, _ := http.NewRequest("GET", "https://tls.peet.ws/api/all", nil)
req.Header.Set("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "+
"AppleWebKit/537.36 (KHTML, like Gecko) "+
"Chrome/120.0.0.0 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Ten kod wykonuje następujące kroki:
- Nawiązuje połączenie TCP z
gate.proxyhat.com:8080 - Wysyła
CONNECTz autoryzacją Basic, w której username zawiera-country-US-session-abc123 - Proxy nawiązuje tunel do docelowego serwera
- Na tunelowanym połączeniu wykonany jest handshake uTLS z profilem
HelloChrome_Auto - Cały ruch HTTP idzie przez proxy residential z IP w USA, z sesją sticky
abc123
Wynik: WAF widzi Chrome JA3/JA4 + residential IP z USA — kompletny, spójny obraz prawdziwego użytkownika. Możesz zmienić -country-US na -country-DE lub dodać -city-berlin dla precyzyjniejszego geo-targetingu. Pełną listę dostępnych lokalizacji znajdziesz na stronie lokalizacji ProxyHat.
Kluczowa zasada: odcisk TLS i reputacja IP muszą być spójne. Chrome JA3 + datacenter IP = podejrzane. Chrome JA3 + residential IP = wiarygodne. Safari JA3 + residential IP w Niemczech + UA Safari na macOS = maksymalna wiarygodność.
JA4 zastępuje JA3 — jak utrzymać aktualność parrotów
JA4 nie jest tylko „JA3 v2" — to fundament nowej generacji fingerprintingu. FoxIO i społeczność threat intelligence migrują w stronę JA4, ponieważ jest bardziej odporny na manipulację i daje lepszą klasyfikację. Cloudflare już integruje JA4 w swoich produktach Bot Management.
Dla inżynierów Go to oznacza dwie rzeczy:
- JA3 spoofing nie wystarczy — musisz dbać o JA4 również. uTLS z
HelloChrome_Autogeneruje ClientHello, który jest poprawny pod kątem obu algorytmów, bo replikuje pełną strukturę, a nie tylko hash JA3. - Profile muszą być aktualne — Chrome zmienia ClientHello co kilka wersji.
HelloChrome_Autoautomatycznie wybiera najnowszy dostępny profil w uTLS, ale musisz regularnie aktualizować zależność:go get -u github.com/refraction-networking/utls.
Najlepsza praktyka: testuj regularnie. Ustaw CI, który codziennie wysyła żądanie do tls.peet.ws/api/all i porównuje zwrócony JA4 z oczekiwanym profilem Chrome. Jeśli odcisk się zmienił — zaktualizuj uTLS.
Dodatkowo, pamiętaj o spójności warstw. Odcisk TLS to jedna warstwa; WAF-y sprawdzają także:
- HTTP/2 fingerprint (JA4H / Akamai HTTP2 fingerprint) — kolejność nagłówków, ustawienia SETTINGS, window update
- Canvas/WebGL fingerprint — jeśli używasz headless browser
- Behavioral analytics — tempo żądań, wzorce nawigacji
- Header order — Go net/http sortuje nagłówki alfabetycznie; Chrome używa specyficznej kolejności
Pełna mimikra wymaga uwzględnienia wszystkich tych warstw. uTLS rozwiązuje TLS; strategie scrapowania i odpowiednia rotacja proxy rozwiązują warstwę sieciową. Więcej o optymalizacji monitoringu SERP znajdziesz w przypadku użycia SERP tracking.
Najlepsze praktyki i pułapki
Spójność profilu i User-Agentu
Najczęstszy błąd: HelloChrome_Auto + UA Safari. WAF-y cross-checkują odcisk TLS z UA. Jeśli JA3 mówi Chrome, UA musi mówić Chrome — inaczej dostajesz wyższy score botowania niż bez uTLS w ogóle.
Nie używaj HelloChrome_Auto na produkcyjnych scraperach bez testów
HelloChrome_Auto może zmienić profil przy aktualizacji uTLS. Jeśli Twoja aplikacja zależy od konkretnego odcisku, używaj HelloChrome_120 (lub konkretnego numeru wersji) i aktualizuj świadomie.
Rotacja sesji proxy
Sticky session (-session-abc123) jest przydatna, gdy musisz utrzymać ten sam IP dla wielu żądań (logowanie, sesje koszyka). Do czystego scrapowania SERP czy cen, używaj rotacji per-request — usuń flagę -session, a ProxyHat przydzieli nowy IP przy każdym żądaniu.
Etyczne i legalne aspekty
TLS fingerprinting i proxy residential to potężne narzędzia. Używaj ich do legalnej automatyzacji — monitorowania cen, researchu SEO, testów bezpieczeństwa autoryzowanych przez właściciela infrastruktury, zbierania danych publicznych zgodnie z robots.txt i regulaminami. Nie używaj do omijania zabezpieczeń w celach nadużycia, kradzieży danych ani naruszania ToS platform. Więcej szczegółów znajdziesz w dokumentacji ProxyHat.
Kluczowe wnioski
- Go net/http ma stały, rozpoznawalny odcisk TLS — brak GREASE, brak paddingu, inna kolejność szyfrów niż Chrome. WAF-y flagują to natychmiast.
- JA3/JA4 to pierwsza linia detekcji — WAF analizuje ClientHello zanim zobaczy jakikolwiek nagłówek HTTP. Sprawdź swój odcisk na
tls.peet.ws/api/all. - uTLS z HelloChrome_Auto rozwiązuje warstwę TLS — pełna replika ClientHello Chrome, z GREASE, paddingiem i poprawną kolejnością rozszerzeń.
- Proxy residential jest niezbędne — odcisk Chrome z IP datacenter to nadal czerwona flaga. Route przez
gate.proxyhat.com:8080z geo-targetingiem i sesjami sticky. - JA4 zastępuje JA3 — aktualizuj uTLS regularnie i testuj odcisk w CI.
- Spójność warstw jest kluczowa — TLS + IP + UA + header order + HTTP/2 fingerprint muszą wszystkie pasować do jednej „osoby".
Gotowy, by zbudować scraper, który przechodzi przez WAF-y? Sprawdź ceny ProxyHat i wybierz pakiet residential proxy odpowiedni dla Twojego obciążenia.






