Por que o fingerprint TLS do net/http no Go é detectado e como corrigi-lo
Se você constrói scrapers ou clientes HTTP em Go, já deve ter esbarrado em um problema frustrante: seu código funciona perfeitamente no localhost, mas no momento em que aponta para um site protegido por Cloudflare ou Akamai, recebe um 403 Forbidden antes mesmo de enviar um header. A causa raramente é o seu User-Agent — é o fingerprint TLS do net/http do Go.
Para corrigir o fingerprint TLS do net/http no Go, você precisa substituir o handshake TLS padrão (que usa crypto/tls) por uma biblioteca como uTLS, que emite um ClientHello idêntico ao do Chrome real. Combinado com proxies residenciais da ProxyHat, seu tráfego passa a ter tanto a assinatura criptográfica quanto a reputação de IP de um usuário legítimo.
Este guia cobre exatamente como a detecção funciona, como capturar seu próprio JA3/JA4, como implementar uTLS com HelloChrome_Auto, e como rotear tudo através de proxies residenciais para máxima confiabilidade em automação legítima e pesquisa de segurança.
O problema técnico: por que o Go difere do Chrome
A biblioteca padrão crypto/tls do Go prioriza correção e simplicidade sobre mimetismo. O ClientHello que ela gera é estático e determinístico — para uma dada versão do Go, cada conexão produz exatamente o mesmo conjunto de cipher suites, extensões e curvas elípticas, na mesma ordem. Não há aleatoriedade intencional.
O Chrome, por outro lado, implementa TLS 1.3 (RFC 8446) com várias camadas de aleatorização que o Go simplesmente não tem:
- GREASE (RFC 8701): valores reservados inseridos em cipher suites, grupos suportados e extensões para evitar problemas de compatibilidade com middleboxes. O Chrome usa 16 valores GREASE diferentes, rotacionados por sessão.
- Ordem de cipher suites específica do Chrome, que difere da ordem alfabética/numérica do Go.
- Extensões adicionais como
encrypted_client_hello,application_settings, erenegotiation_infoque o Go não envia por padrão. - Key share com curva X25519 e X25519Kyber768Draft00 (pós-quântico) que o Go só suporta parcialmente.
O resultado é um JA3/JA4 completamente diferente do de qualquer navegador real. WAFs como Cloudflare e Akamai mantêm bancos de dados de fingerprints conhecidos e bloqueiam automaticamente qualquer ClientHello que pareça automatizado — e o fingerprint do Go é um dos mais reconhecíveis. Estudos indicam que mais de 50% dos sites de e-commerce usam proteção anti-bot baseada em fingerprint TLS.
Capturando seu próprio JA3 e JA4
Antes de tentar corrigir qualquer coisa, meça o problema. O site tls.peet.ws retorna um JSON completo com seu JA3, JA4, e todos os detalhes do ClientHello. Execute este comando:
curl -s https://tls.peet.ws/api/all | jq '.tls.ja3, .tls.ja4'
Se você fizer a mesma requisição com Go puro, verá um hash JA3 completamente diferente. Aqui está um exemplo mínimo:
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
)
func main() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
client := &http.Client{Transport: tr}
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))
}
Anote o hash JA3 e JA4 retornados. Provavelmente você verá algo como 19e29534fd99f24 (JA3) — um fingerprint que aparece em listas de bloqueio públicas. Esse é o sinal de que seu Go está sendo identificado antes mesmo de enviar dados.
Decompondo os sinais do ClientHello: Go vs Chrome
A tabela abaixo contrasta os principais campos do ClientHello que diferem entre o Go padrão e o Chrome 120+:
| Sinal do ClientHello | Go net/http (padrão) |
Chrome 120+ |
|---|---|---|
| Cipher suites (ordem) | Fixa, numérica: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256 |
GREASE primeiro, depois TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256 com GREASE intercalado |
| supported_groups | x25519, secp256r1, secp384r1 (sem GREASE) |
GREASE, x25519, secp256r1, x25519Kyber768Draft00, secp384r1 (com GREASE) |
| signature_algorithms | ECDSAWithP256AndSHA256, PKCS1WithSHA256, ECDSAWithP384AndSHA384, ... |
ecdsa_secp256r1_sha256, rsa_pss_rsae_sha256, rsa_pkcs1_sha256, ... (ordem diferente, 16 algoritmos) |
| ALPN | h2, http/1.1 |
h2, http/1.1 (mesmo, mas com GREASE antes) |
| key_share | x25519, secp256r1 |
GREASE, x25519, x25519Kyber768Draft00 |
| Extensões extras | Mínimas (~12 extensões) | ~22 extensões incluindo encrypted_client_hello, application_settings, delegated_credentials |
| GREASE | Ausente | Presente em 4 posições, rotacionado por sessão |
Cada uma dessas diferenças é um sinal que um WAF pode verificar. O Cloudflare, por exemplo, não precisa bloquear todos os sinais — basta um campo inesperado (como a ausência de GREASE) para classificar a conexão como não-navegador. A latência de detecção é inferior a 200ms em edge nodes.
O que é JA3 e por que JA4 está substituindo
O JA3 é um hash MD5 calculado sobre os campos TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats do ClientHello, concatenados em uma string específica. É simples, mas tem problemas: dois ClientHellos com ordens de cipher diferentes produzem o mesmo hash, e o MD5 é vulnerável a colisões.
O JA4 supera essas limitações usando SHA-256 e preservando a ordem dos campos. O formato é JA4_t12_0002 onde cada segmento codifica versão TLS, cipher suites (ordenados), extensões (ordenados) e algoritmos de assinatura. O JA4 também introduz variantes como JA4H (HTTP) e JA4S (Server), cobrindo toda a sessão TLS.
Para manter seus "parrots" (mímicas de fingerprint) atuais, você precisa acompanhar as mudanças do Chrome. O uTLS atualiza HelloChrome_Auto periodicamente, mas versões muito antigas podem não incluir extensões como x25519Kyber768Draft00. Verifique a data do último commit do repositório uTLS antes de fixar uma versão. O Chrome atualiza a cada 6 semanas aproximadamente.
Implementando uTLS: HelloChrome_Auto e HelloSafari_16_0
O refraction-networking/utls é um fork de crypto/tls que permite especificar um ClientHello preset. O preset HelloChrome_Auto acompanha a versão mais recente do Chrome, enquanto HelloSafari_16_0 é útil quando você precisa simular Safari especificamente.
A integração com net/http exige substituir o DialTLSContext do http.Transport. Aqui está o padrão completo:
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
utls "github.com/refraction-networking/utls"
)
func main() {
dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
// Conecta TCP diretamente
rawConn, err := (&net.Dialer{}).DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
// Cria UClient com fingerprint do Chrome
uConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
}, utls.HelloChrome_Auto)
// Handshake
if err := uConn.HandshakeContext(ctx); err != nil {
rawConn.Close()
return nil, err
}
return uConn, nil
}
tr := &http.Transport{
DialTLSContext: dialTLS,
ForceAttemptHTTP2: true,
}
client := &http.Client{Transport: tr}
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))
}
Após rodar este código, o JA3 retornado por tls.peet.ws deve corresponder ao do Chrome. Se ainda não corresponder, verifique se está usando a versão mais recente do uTLS — o preset HelloChrome_Auto é atualizado conforme o Chrome evolui.
Usando HelloSafari_16_0
Para casos onde o alvo espera tráfego Safari (por exemplo, serviços Apple específicos), troque o preset:
uConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
}, utls.HelloSafari_16_0)
Cada preset carrega um conjunto diferente de cipher suites, extensões e curvas. Escolha o que melhor corresponde ao tráfego que você está imitando. O fingerprint TLS do Go difere do Chrome em pelo menos 6 categorias de campos, e cada preset do uTLS corrige todas elas simultaneamente.
Alternativas de nível mais alto: CycleTLS e azuretls-client
Se você não quer gerenciar DialTLSContext manualmente, duas bibliotecas encapsulam uTLS com APIs mais simples:
| Biblioteca | Abordagem | Vantagens | Limitações |
|---|---|---|---|
| CycleTLS | Wrapper Go/Node.js sobre uTLS, com API estilo fetch | Simples de usar, suporta JA3 customizado, cookies e headers automáticos | Menos flexível para configurações finas de Transport |
| azuretls-client | Cliente HTTP completo em Go com uTLS integrado | Suporta proxies nativamente, multi-fingerprint, thread-safe, ordenação de headers | API menos padrão, curva de aprendizado para migração |
| uTLS puro | Substitui diretamente o handshake TLS | Controle total, integração nativa com net/http |
Requer boilerplate de DialTLSContext |
Para automação rápida e protótipos, CycleTLS é excelente. Para produção com controle granular, uTLS puro com DialTLSContext é a melhor escolha. O azuretls-client é um meio-termo interessante quando você precisa de ordenação de headers HTTP além do fingerprint TLS.
Combinando uTLS com proxies residenciais ProxyHat
Mimetizar o fingerprint TLS é metade da batalha. Se o IP de saída for um datacenter conhecido (AWS, DigitalOcean, Hetzner), o WAF pode bloquear mesmo com um JA3 perfeito. A reputação de IP é tão importante quanto a assinatura criptográfica.
A ProxyHat oferece proxies residenciais que fornecem IPs de ISPs reais, fazendo seu tráfego parecer de um usuário doméstico legítimo. Combinado com uTLS, você obtém consistência em três camadas:
- Camada de rede: IP residencial com reputação limpa
- Camada criptográfica: JA3/JA4 idêntico ao Chrome
- Camada HTTP: headers e ordem consistentes com o navegador imitado
Aqui está um exemplo completo que conecta um cliente uTLS através de um proxy residencial ProxyHat com geo-targeting US e sessão fixa:
package main
import (
"context"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
utls "github.com/refraction-networking/utls"
)
func main() {
// Credenciais ProxyHat com geo-targeting US e sessão fixa
proxyUser := "user-country-US-session-abc123"
proxyPass := "pass"
proxyHost := "gate.proxyhat.com:8080"
dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
// Conecta ao proxy HTTP da ProxyHat
dialer := &net.Dialer{Timeout: 15 * time.Second}
proxyConn, err := dialer.DialContext(ctx, "tcp", proxyHost)
if err != nil {
return nil, fmt.Errorf("proxy dial: %w", err)
}
// Envia CONNECT para o destino
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 = proxyConn.Write([]byte(connectReq))
if err != nil {
proxyConn.Close()
return nil, err
}
// Lê resposta do proxy (simplificado)
buf := make([]byte, 1024)
n, _ := proxyConn.Read(buf)
respLine := string(buf[:n])
if len(respLine) < 12 || respLine[9:12] != "200" {
proxyConn.Close()
return nil, fmt.Errorf("proxy CONNECT falhou: %s", respLine)
}
// TLS handshake com fingerprint Chrome
uConn := utls.UClient(proxyConn, &utls.Config{
ServerName: host,
}, utls.HelloChrome_Auto)
if err := uConn.HandshakeContext(ctx); err != nil {
proxyConn.Close()
return nil, err
}
return uConn, nil
}
tr := &http.Transport{
DialTLSContext: dialTLS,
ForceAttemptHTTP2: true,
}
client := &http.Client{
Transport: tr,
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))
}
Com este setup, o tls.peet.ws deve reportar um JA3/JA4 correspondente ao Chrome e um IP residencial dos EUA — exatamente o que um usuário real produziria.
Para outros cenários de geo-targeting, ajuste o usuário do proxy:
// Alemanha, Berlim
proxyUser := "user-country-DE-city-berlin"
// Japão, sessão fixa
proxyUser := "user-country-JP-session-xyz789"
Consulte a lista completa de localizações disponíveis em /pt/locations. Para detalhes de configuração avançada, veja a documentação oficial.
Erros comuns e casos extremos
1. Esquecer o HTTP/2
O Chrome usa HTTP/2 quase sempre. Se seu http.Transport não tem ForceAttemptHTTP2: true, o Go pode negociar HTTP/1.1, que por si só é um sinal de não-navegador para alguns WAFs. O Chrome 120+ usa HTTP/2 em mais de 95% das conexões.
2. Headers fora de ordem
O uTLS corrige o ClientHello, mas não os headers HTTP. O net/http do Go envia headers em ordem alfabética, enquanto o Chrome envia em uma ordem específica (Host, Connection, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, Upgrade-Insecure-Requests, User-Agent, ...). Bibliotecas como azuretls-client resolvem isso; com uTLS puro, você precisa manipular a ordem dos headers manualmente usando http.Header.Set com cuidado, ou escrever um Transport.RoundTrip customizado.
3. Sessões TLS não reutilizadas
Navegadores reais reusam sessões TLS (session resumption via tickets). O Go com uTLS puro cria uma nova sessão a cada conexão por padrão, o que é incomum para tráfego de navegador. Para mitigar, implemente um pool de conexões com MaxIdleConns e IdleConnTimeout razoáveis (ex: MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second).
4. Presets desatualizados
O HelloChrome_Auto de versões antigas do uTLS pode não incluir extensões recentes como x25519Kyber768Draft00. Sempre use a versão mais recente do uTLS e monitore mudanças no ClientHello do Chrome a cada 3–6 meses. Um fingerprint de Chrome 110 enviado como se fosse Chrome 120 é um sinal de automação tão óbvio quanto o fingerprint do Go padrão.
Práticas recomendadas para automação legítima
Quando você realiza scraping legítimo, pesquisa de segurança autorizada ou automação de serviços que você tem permissão para acessar, a combinação uTLS + proxies residenciais é uma ferramenta essencial. Algumas diretrizes:
- Respeite
robots.txte os Termos de Serviço do alvo. - Use rate limiting responsável — 1–2 requisições por segundo por sessão é um bom ponto de partida.
- Rotacione sessões ProxyHat para distribuir carga e evitar padrões detectáveis.
- Monitore a taxa de sucesso (
2xxvs403/429) e ajuste a concorrência conforme necessário. Uma taxa de bloqueio acima de 5% indica que algo precisa de ajuste. - Para SERP tracking, veja nosso guia em /pt/use-cases/serp-tracking.
- Para web scraping em geral, consulte /pt/use-cases/web-scraping.
Principais conclusões
O fingerprint TLS do
net/httpdo Go é estático e reconhecível — WAFs o detectam em milissegundos. A correção envolve três camadas: substituir o handshake com uTLS (HelloChrome_Auto), alinhar os headers HTTP com o navegador imitado, e rotear o tráfego através de proxies residenciais com reputação de IP limpa.
- Go
crypto/tlsé detectável: falta GREASE, ordem de ciphers diferente, ~12 extensões vs ~22 do Chrome. - uTLS resolve a camada TLS:
HelloChrome_Autoreplica o ClientHello do Chrome atual;HelloSafari_16_0para casos específicos. - JA4 supera JA3: SHA-256, preserva ordem dos campos, mais resistente a colisões.
- IP reputation importa tanto quanto JA3: combine uTLS com proxies residenciais ProxyHat em
gate.proxyhat.com:8080. - Mantenha presets atualizados: o Chrome muda a cada 6 semanas; verifique o uTLS a cada 3–6 meses.
Para começar com proxies residenciais da ProxyHat, visite /pt/pricing ou explore casos de uso em /pt/use-cases/web-scraping. A documentação completa de integração está em docs.proxyhat.com.






