Se scrivi scraper o client HTTP in Go e improvvisamente ricevi HTTP 403, sfide JavaScript o redirect infiniti verso cdn-cgi/challenge-platform, il colpevole quasi mai è il tuo User-Agent. È il fingerprint TLS di Go net/http: il modo in cui crypto/tls costruisce il ClientHello è così diverso da un browser reale che Cloudflare, Akamai e Datadome lo classificano come bot in meno di 200 ms. Questa guida mostra come correggere il fingerprint TLS di Go net/http con refraction-networking/utls, confronta alternative come CycleTLS e azuretls-client, e spiega perché persino il miglior parrot TLS fallisce senza un'identità di rete pulita.
Perché il fingerprint TLS di Go net/http viene bloccato
Il problema esiste perché crypto/tls è progettato per correttezza crittografica, non per mimetismo. Il ClientHello che Go emette è deterministico: ordine dei cipher suite fisso, nessun valore GREASE, estensioni minime, supported_groups e signature_algorithms statici. Il risultato è un JA3/JA4 inconfondibile che non corrisponde a nessun browser mainstream.
I WAF moderni non si fidano dell'header User-Agent: lo usano solo come sanity check. Il segnale primario è la firma TLS, perché è molto più difficile da falsificare. Quando Cloudflare vede un JA3 tipo 771,4865-4866-4867-49195-49199-... con cipher order di Go invece dell'ordine Chrome con GREASE, il verdetto è immediato: bot.
Per misurare il tuo fingerprint attuale, punta il tuo client Go contro l'API pubblica di Peet.ws:
curl https://tls.peet.ws/api/all -H "User-Agent: Mozilla/5.0" | jq '.tls.ja3,.tls.ja4'
Ripeti lo stesso test con un net/http client Go bare-metal e confronta. Vedrai che il JA3 di Go è una stringa corta e prevedibile, mentre quello di Chrome è lungo, contiene valori GREASE come 2570 e 29223, e include estensioni come application_settings e encrypted_client_hello. Documentazione di riferimento per le estensioni TLS: RFC 8446.
I segnali del ClientHello che differiscono tra Go e Chrome
Il fingerprint TLS non è un singolo numero: è la combinazione di diversi campi del ClientHello. Ecco i cinque segnali che tradiscono Go.
Cipher suite order
Go ordina i cipher in modo canonico (TLS 1.3 prima, poi ECDHE, poi CBC). Chrome usa un ordine specifico con valori GREASE sparsi in posizioni pseudo-casuali ma deterministiche per sessione. Il JA3 di Go tipicamente inizia con 771,4865,4866,4867,49195,49199,..., mentre Chrome inizia con 771,4865,4866,4867,49195,49199,52393,52392,...,2570 dove 2570 è GREASE.
supported_groups
Go invia x25519,secp256r1,secp384r1. Chrome invia x25519,secp256r1,x25519Kyber768Draft00,secp384r1 — nota il gruppo post-quantum Kyber che Go non include. Questo da solo cambia il JA4.
signature_algorithms
Go: ecdsa_secp256r1_sha256,rsa_pss_rsae_sha256,rsa_pkcs1_sha256,.... Chrome: ordine diverso, include ed25519 e rsa_pss_pss_sha256 in posizioni specifiche.
ALPN e key_share
Go offre h2,http/1.1; Chrome offre h2,http/1.1 ma con application_settings e encrypted_client_hello come estensioni aggiuntive. Il key_share di Go contiene solo x25519; Chrome include x25519Kyber768Draft00 oltre a x25519.
GREASE
I valori GREASE (definiti in RFC 8701) sono cipher/group/extension fittizi che i browser inseriscono per prevenire ossidazione dei middlebox. Go non li emette affatto. La loro assenza è di per sé un segnale: un client che dice di essere Chrome ma non ha GREASE è una bugia immediatamente rilevabile.
| Segnale | Go net/http | Chrome reale |
|---|---|---|
| Cipher order | Canonico, no GREASE | Con GREASE in posizioni pseudo-casuali |
| supported_groups | x25519, secp256r1, secp384r1 | + x25519Kyber768Draft00 |
| signature_algorithms | Senza ed25519 in posizioni Chrome | Include ed25519, ordine Chrome-specific |
| ALPN extensions | h2, http/1.1 base | + application_settings, ECH |
| key_share | x25519 solo | x25519 + Kyber768 draft |
Correggere il fingerprint con uTLS
La soluzione standard è refraction-networking/utls, un fork di crypto/tls che permette di specificare un ClientHelloID predefinito. utls.HelloChrome_Auto segue l'ultima versione di Chrome disponibile; per il caso "imposta il fingerprint TLS a Safari" usa utls.HelloSafari_16_0.
Il pattern è: creare un utls.UClient, impostare il ClientHelloSpec desiderato, e collegarlo a un http.Transport tramite DialTLSContext. Ecco un esempio minimale:
package main
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"time"
utls "github.com/refraction-networking/utls"
)
type uTLSRoundTripper struct {
clientHelloID utls.ClientHelloID
proxyURL string
}
func (r *uTLSRoundTripper) dialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
config := &utls.Config{ServerName: host}
uConn := utls.UClient(nil, config, r.clientHelloID)
if err := uConn.BeginHandshake(); err != nil {
return nil, err
}
// In produzione: connetti tramite proxy prima del handshake
return uConn, nil
}
func main() {
rt := &uTLSRoundTripper{clientHelloID: utls.HelloChrome_Auto}
transport := &http.Transport{DialTLSContext: rt.dialTLSContext}
client := &http.Client{Transport: transport, Timeout: 30 * time.Second}
resp, err := client.Get("https://tls.peet.ws/api/all")
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status)
}
Dopo aver eseguito questo codice, confronta il JA3/JA4 restituito da tls.peet.ws/api/all con quello di un Chrome reale: dovrebbero coincidere o differire solo per i valori GREASE casuali, che è il comportamento atteso.
Nota su HelloSafari_16_0: se il tuo target usa un WAF che profila Safari (tipico per e-commerce asiatici), usa utls.HelloSafari_16_0 e abbina un User-Agent Safari coerente. Mismatch tra JA3 Safari e UA Chrome viene rilevato.
Alternative di livello superiore: CycleTLS e azuretls-client
Se non vuoi gestire manualmente DialTLSContext, due librerie di livello superiore incapsulano uTLS.
CycleLS
CycleTLS espone un'API semplice con Index.Cycle(...) dove specifichi JA3, User-Agent e cookie per richiesta. È comodo per scraping one-shot ma il suo JA3 è una stringa che devi aggiornare manualmente quando Chrome cambia.
azuretls-client
azuretls-client è un client HTTP completo con uTLS integrato, supporto HTTP/2, cookie jar e proxy chaining. Rispetto a CycleTLS è più vicino a un drop-in replacement di net/http:
session := azuretls.NewSession()
session.Browser = azuretls.Chrome
resp, err := session.Get("https://tls.peet.ws/api/all")
| Libreria | Livello | Aggiornamento JA3 | HTTP/2 nativo |
|---|---|---|---|
| uTLS puro | Basso (Transport) | Automatico con HelloChrome_Auto | Manuale |
| CycleLS | Medio | Manuale (stringa JA3) | Parziale |
| azuretls-client | Alto (Session) | Automatico con Browser=Chrome | Sì |
Perché il mimetismo TLS da solo fallisce: identità di rete
Correggere il fingerprint TLS di Go net/http risolve il primo gate, ma i WAF moderni combinano segnali TLS con reputazione IP. Un ClientHello perfettamente Chrome-like che arriva da un IP datacenter di Hetzner o OVH viene comunque flaggato: il JA3 è umano, ma l'IP no.
Per superare il secondo gate devi instradare il client uTLS attraverso proxy residenziali con IP ASN di ISP domestici. ProxyHat fornisce gateway HTTP su gate.proxyhat.com:8080 e SOCKS5 su gate.proxyhat.com:1080, con geo-targeting e sessioni sticky nel campo username.
Il pattern corretto: connettiti al proxy ProxyHat come tunnel TCP, poi esegui l'handshake uTLS attraverso quel tunnel. Ecco un esempio completo che diala via proxy HTTP con sessione sticky USA:
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
utls "github.com/refraction-networking/utls"
)
func dialThroughProxy(ctx context.Context, proxyAddr, targetHost string) (net.Conn, error) {
// Connetti al proxy HTTP di ProxyHat
proxyConn, err := net.DialTimeout("tcp", proxyAddr, 15*time.Second)
if err != nil {
return nil, err
}
// CONNECT request al proxy
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", targetHost, targetHost)
if _, err := proxyConn.Write([]byte(connectReq)); err != nil {
proxyConn.Close()
return nil, err
}
// Leggi risposta 200 Connection Established (semplificato)
buf := make([]byte, 1024)
if _, err := proxyConn.Read(buf); err != nil {
proxyConn.Close()
return nil, err
}
return proxyConn, nil
}
func main() {
proxyUser := url.QueryEscape("user-country-US-session-abc123")
proxyPass := url.QueryEscape("pass")
proxyAddr := "gate.proxyhat.com:8080"
target := "tls.peet.ws:443"
transport := &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := dialThroughProxy(ctx, proxyAddr, target)
if err != nil {
return nil, err
}
config := &utls.Config{ServerName: "tls.peet.ws"}
uConn := utls.UClient(conn, config, utls.HelloChrome_Auto)
if err := uConn.HandshakeContext(ctx); err != nil {
conn.Close()
return nil, err
}
return uConn, nil
},
}
_ = proxyUser
_ = proxyPass
client := &http.Client{Transport: transport, Timeout: 30 * 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)[:200])
}
Per l'autenticazione proxy, includi le credenziali nell'header Proxy-Authorization del CONNECT o usa il formato URL http://user-country-US-session-abc123:pass@gate.proxyhat.com:8080 con una libreria che gestisce il proxy HTTP nativamente. La flag country-US forza IP residenziali USA; session-abc123 mantiene lo stesso IP per richieste successive, essenziale per login flow e scraping multi-pagina che richiede coerenza di sessione.
Per scenari SERP e scraping e-commerce, combina utls.HelloChrome_Auto con proxy residenziali geo-targeted. Vedi i paesi disponibili e i piani ProxyHat.
JA4 sta sostituendo JA3: come mantenere i parrot aggiornati
JA3 è stato lo standard per anni ma ha limiti: ordina i cipher in modo che due client con cipher diversi ma stesso set producano lo stesso hash. JA4, introdotto da FoxIO nel 2023, risolve questo segmentando il fingerprint in quattro componenti separate (JA4_a, JA4_b, JA4_c, JA4_d) e usando un formato che preserva l'ordine. JA4 è già usato da strumenti come Suricata e da alcuni WAF enterprise.
Per restare aggiornato:
- Verifica il tuo JA4 con
tls.peet.ws/api/alldopo ogni release di uTLS. - Aggiorna
refraction-networking/utlsquando esce una nuova versione di Chrome che cambiasupported_groupso cipher order. Tipicamente ogni 4-6 settimane. - Monitora il changelog uTLS per nuove
HelloChrome_*spec. - Se il tuo target passa a JA4-only, assicurati che il tuo parrot produca un JA4 valido, non solo un JA3 che sembra Chrome.
Una strategia robusta è eseguire un test automatico CI che confronta il JA3/JA4 del tuo client Go-uTLS contro quello di un browser Chrome headless reale (via Puppeteer o Playwright) su tls.peet.ws/api/all. Se divergono oltre i valori GREASE, fallisci il build.
Considerazioni etiche e legali
Il mimetismo TLS e l'uso di proxy residenziali sono tecniche neutre: dipendono dall'uso. Applicazioni legittime includono ricerca di sicurezza autorizzata, pentesting con scope definito, monitoraggio prezzi pubblico per analisi competitiva, e automazione di API pubbliche che non violano i ToS. Rispetta robots.txt, i rate limit ragionevoli e il GDPR/CCPA quando elabori dati personali. Non usare queste tecniche per aggirare paywall, credential stuffing, o attacchi DDoS. Per approfondire i casi d'uso legittimi vedi web scraping e SERP tracking.
Errori comuni e edge case
- Mismatch UA / JA3: invii User-Agent Chrome ma
HelloFirefox_105. I WAF incrociano i due: usa lo stesso browser per entrambi. - Dimenticare HTTP/2 fingerprint: anche con uTLS corretto, l'HTTP/2 SETTINGS frame di Go differisce da Chrome (SETTINGS order, WINDOW_UPDATE values). azuretls-client gestisce questo; uTLS puro no.
- Proxy datacenter con JA3 Chrome: il WAF vede JA3 Chrome + IP OVH = bot con maschera. Usa sempre residenziali per target protetti.
- Sessioni non sticky su flow multi-richiesta: login → dashboard → dati richiede lo stesso IP. Usa
session-abc123nel username ProxyHat. - Timeout handshake su proxy lenti: imposta
HandshakeContextcon deadline adeguata (15-30 s) e retry con backoff.
Punti chiave
- Il fingerprint TLS di Go net/http è statico e non simile a Chrome: niente GREASE, cipher order canonico, estensioni mancanti.
- I WAF usano JA3/JA4 come segnale primario; l'User-Agent è solo un sanity check.
refraction-networking/utlsconHelloChrome_Autocorregge il ClientHello;HelloSafari_16_0per il caso Safari.- CycleLS e azuretls-client sono alternative di livello superiore; azuretls gestisce anche HTTP/2 fingerprint.
- Il mimetismo TLS fallisce senza IP pulito: route uTLS attraverso proxy residenziali ProxyHat su
gate.proxyhat.com:8080con geo-targeting e sessioni sticky. - JA4 sta sostituendo JA3: automatizza il confronto del fingerprint contro Chrome reale in CI.
Per iniziare, registra un account su ProxyHat, configura gate.proxyhat.com:8080 con user-country-US-session-abc123, e punta il tuo client uTLS contro tls.peet.ws/api/all per verificare che JA3, JA4 e IP ASN combacino con un profilo umano coerente. Documentazione completa: docs.proxyhat.com.






