Correggere il fingerprint TLS di Go net/http: guida pratica con uTLS e proxy residenziali

Il client HTTP standard di Go viene bloccato dai WAF perché crypto/tls genera un ClientHello statico e non simile a Chrome. Scopri come correggerlo con uTLS, CycleTLS, azuretls e proxy residenziali ProxyHat.

Fixing Go's net/http TLS Fingerprint: Pass WAF Detection with uTLS
In questo articolo

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.

SegnaleGo net/httpChrome reale
Cipher orderCanonico, no GREASECon GREASE in posizioni pseudo-casuali
supported_groupsx25519, secp256r1, secp384r1+ x25519Kyber768Draft00
signature_algorithmsSenza ed25519 in posizioni ChromeInclude ed25519, ordine Chrome-specific
ALPN extensionsh2, http/1.1 base+ application_settings, ECH
key_sharex25519 solox25519 + 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")
LibreriaLivelloAggiornamento JA3HTTP/2 nativo
uTLS puroBasso (Transport)Automatico con HelloChrome_AutoManuale
CycleLSMedioManuale (stringa JA3)Parziale
azuretls-clientAlto (Session)Automatico con Browser=Chrome

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/all dopo ogni release di uTLS.
  • Aggiorna refraction-networking/utls quando esce una nuova versione di Chrome che cambia supported_groups o 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-abc123 nel username ProxyHat.
  • Timeout handshake su proxy lenti: imposta HandshakeContext con 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/utls con HelloChrome_Auto corregge il ClientHello; HelloSafari_16_0 per 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:8080 con 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.

Domande frequenti

Cos'è il fingerprint TLS di Go net/http e perché viene bloccato?

Il fingerprint TLS di Go net/http è la firma del ClientHello generata da crypto/tls. È statico: ordine cipher canonico, nessun valore GREASE, estensioni minime, supported_groups senza Kyber. Questo produce un JA3/JA4 che non corrisponde a nessun browser reale, quindi i WAF come Cloudflare e Akamai lo classificano come bot in meno di 200 ms, indipendentemente dall'User-Agent inviato.

Perché il fingerprint TLS di Go importa per chi usa proxy?

Anche con proxy residenziali puliti, se il fingerprint TLS di Go resta quello nativo il WAF rileva l'incongruenza: IP umano ma JA3 da bot. Il fingerprint TLS è il primo gate che i WAF valutano, prima ancora della reputazione IP. Correggere il JA3/JA4 con uTLS è prerequisito perché i proxy residenziali abbiano effetto sui target protetti.

Quale tipo di proxy funziona meglio con uTLS in Go?

Proxy residenziali con geo-targeting e sessioni sticky. I proxy datacenter hanno ASN noti (Hetzner, OVH, AWS) che i WAF flaggano anche con JA3 Chrome perfetto. I residenziali di ProxyHat su gate.proxyhat.com:8080 offrono IP ISP domestici, e la flag session-abc123 mantiene lo stesso IP per flow multi-richiesta, essenziale per login e scraping multi-pagina.

Come evitare blocchi implementando uTLS in Go?

Usa utls.HelloChrome_Auto per seguire l'ultima versione Chrome, abbina un User-Agent Chrome coerente, route il traffico attraverso proxy residenziali ProxyHat con country targeting, e automatizza un test CI che confronta il tuo JA3/JA4 contro Chrome reale via tls.peet.ws/api/all. Verifica anche l'HTTP/2 fingerprint, non solo TLS: azuretls-client lo gestisce nativamente mentre uTLS puro no.

JA4 sostituirà JA3 e come rimanere aggiornati?

JA4 segmenta il fingerprint in quattro componenti e preserva l'ordine dei cipher, risolvendo le collisioni di JA3. È già usato da Suricata e WAF enterprise. Per restare aggiornato: aggiorna refraction-networking/utls ogni 4-6 settimane, monitora il changelog per nuove HelloChrome_* spec, e testa il JA4 su tls.peet.ws/api/all dopo ogni release.

Metti alla prova i tuoi proxy contro vere difese anti-bot

Verificatore di proxy gratuito — latenza, anonimato e segnali di blocco in un clic.

Fai un test gratuito
← Torna al Blog