GoSuda

gosuda.org/chromenet: Pilha de Rede do Chromium para Go, Construída de Forma Reproduzível a partir de uma Versão Fixada

By Lemon Mint views ...

gosuda.org/chromenet é um http.RoundTripper em Go que envia requisições pela própria pilha de rede do Chromium. A pilha é //net de uma versão oficial do Chromium: suas implementações de HTTP/1.1, HTTP/2 e QUIC/HTTP/3, o BoringSSL, a configuração TLS do Chrome e o Chrome Root Store. Ela roda em um processo de trabalho separado, chrome_net_server, e o lado Go permanece Go puro (CGO_ENABLED=0), comunicando-se com o processo de trabalho por RPC de memória compartilhada HQQ.

1go get gosuda.org/chromenet@v0.1.10157081033800

Por que Executar a Pilha de Rede do Chromium

Um programa Go que acessa a web com net/http apresenta um cliente Go ao servidor. Esse é o padrão correto para o tráfego entre serviços. Não é adequado quando o comportamento do servidor depende do tipo de cliente que ele acredita estar atendendo: endpoints que exigem um navegador, middlewares anti-bot, CDNs que entregam versões de protocolo diferentes conforme as impressões digitais de TLS e HTTP/2, e testes que precisam reproduzir a experiência de um navegador real.

A biblioteca padrão do Go não consegue reproduzir o comportamento de rede de um navegador. O crypto/tls possui sua própria ordem de suítes de cifras e seu próprio conjunto de extensões, e o net/http possui suas próprias configurações de HTTP/2. Bibliotecas que imitam um navegador copiando os bytes de seu ClientHello continuam precisando ser copiadas a cada nova versão. O chromenet segue outro caminho: não imita a pilha de rede do Chromium, mas a executa.

Medido na engine darwin/arm64 com um serviço público de impressão digital (tls.peet.ws), a configuração padrão reporta:

PropriedadeValor observado
Protocolo negociadoh2
JA4t13d1516h2_8daaf6152771_806a8c22fdea
Impressão digital HTTP/2 (formato Akamai)1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
User-AgentChrome de desktop na versão principal da engine, por exemplo Chrome/157.0.0.0

Com ForceHTTP3, as requisições para cloudflare-quic.com e www.google.com foram respondidas por HTTP/3. Esses números provêm de uma única plataforma em um único dia. A engine é a própria pilha, portanto eles mudam quando a versão fixada do Chromium muda.

Como Usar

NewTransport retorna um http.RoundTripper. Use-o onde você usaria um http.Transport:

 1package main
 2
 3import (
 4	"fmt"
 5	"io"
 6	"net/http"
 7
 8	"gosuda.org/chromenet"
 9)
10
11func main() {
12	client := &http.Client{Transport: chromenet.NewTransport(chromenet.Options{})}
13
14	resp, err := client.Get("https://example.com/")
15	if err != nil {
16		panic(err)
17	}
18	defer resp.Body.Close()
19
20	body, _ := io.ReadAll(resp.Body)
21	fmt.Println(resp.Proto, resp.StatusCode, len(body))
22}

Na primeira requisição, o módulo grava o binário embutido do processo de trabalho para a sua plataforma no diretório de cache do usuário, verifica seu digest SHA-256 com gosuda.org/hqq/secureexec, inicia-o com um vínculo de vida com o processo pai (--parent-stdin, para que ele encerre quando o seu processo encerrar) e abre um pool de sessões. Nada precisa ser instalado.

Alguns comportamentos seguem o http.Transport para que o http.Client funcione sem alterações:

  • O Transport não segue redirecionamentos e não mantém cookies. O http.Client faz ambos, como ocorre com o http.Transport.
  • Respostas comprimidas com gzip, deflate, Brotli ou zstd chegam decodificadas, e resp.Uncompressed é verdadeiro.
  • Uma requisição mantém uma sessão da engine até que o corpo de sua resposta seja fechado. Feche os corpos.
  • Cancelar o contexto da requisição cancela a requisição dentro do Chromium.
  • Falhas de rede são valores *client.NetError que carregam o código e o nome do erro do Chromium:
1var ne *client.NetError
2if errors.As(err, &ne) {
3	fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
4}

Opções

CampoEfeito
UserAgentSubstitui o User-Agent padrão do Chrome. Um cabeçalho User-Agent da requisição continua prevalecendo.
AcceptLanguageSubstitui en-US,en;q=0.9.
ProxyRegras de proxy do Chromium, como http://127.0.0.1:8080 ou socks5://127.0.0.1:1080. Vazio significa conexão direta.
DisableHTTP2, DisableQUICDesativam os protocolos.
ForceHTTP3Usa QUIC para todas as origens, sem aguardar um anúncio Alt-Svc.
InsecureSkipVerifyAceita qualquer certificado de servidor.
PoolSessões da engine a serem usadas. Nil usa client.DefaultPool(), que permite 64 requisições simultâneas.

Transports com opções iguais compartilham um único URLRequestContext do Chromium, de modo que pools de conexões, sessões HTTP/2 e sessões QUIC são reutilizados entre as requisições. Definir HQQ_CHROME_NET_SERVER=/caminho/para/chrome_net_server substitui o processo de trabalho embutido, por exemplo por uma build local.

Como Funciona

CamadaO que é executado ali
Seu programahttp.Client e chromenet.Transport, em Go puro.
Fronteira de processoRPC de memória compartilhada HQQ. A interface é chrome_net.idl, compilada por hqq-bindgen no cliente Go e na camada de integração do servidor em C.
chrome_net_serverUma pequena camada em C++ sobre o Chromium: uma thread de rede que possui todos os objetos de //net, e threads de sessão que bloqueiam uma requisição até que a thread de rede responda.
Chromium //netURLRequestContext, URLRequest, sessões HTTP/2 e QUIC, BoringSSL.

A IDL possui seis operações relevantes: criar uma engine a partir de opções chave=valor; iniciar uma requisição com seu cabeçalho (MÉTODO URL mais as linhas de cabeçalho) e o tamanho do corpo; escrever os blocos do corpo da requisição; aguardar o cabeçalho da resposta; ler o corpo decodificado; e destruir os handles. Um handle de requisição declara @cancel, de modo que uma chamada abandonada interrompe a requisição dentro do Chromium, em vez de deixá-la ser executada até o fim.

Isolar a pilha em um processo de trabalho é uma propriedade de segurança tanto quanto de empacotamento. O código de rede do Chromium interpreta entradas hostis provenientes da internet aberta. Se ele falhar, o seu serviço Go não falha junto: a chamada retorna um erro, a sessão é descartada, e o cliente inicia um novo processo de trabalho na próxima vez que precisar dele.

Construído de Forma Reproduzível, Fixado por SHA-256

Um binário de 25 MB que contém o Chromium só é confiável se for possível afirmar exatamente o que entrou nele. O chromenet acompanha o Chromium Stable da mesma forma que o gosuda.org/sqlite3 acompanha o SQLite: o código-fonte nunca é vendorizado, apenas fixado.

FixaçãoOnde
Versão do Chromium e seu arquivo oficial de código-fonte -litethird_party/chromium/VERSION
SHA-256 do arquivo e de cada ferramenta de build que a versão fixa: clang, Rust, GN, ninja, o runtime do clang para Windows, xwinthird_party/chromium/SHA256
Versões do MSVC CRT e do Windows SDK obtidas via xwinthird_party/chromium/WINSDK
A versão do runtime de sandbox (rungv), o digest da imagem guest, a data do snapshot do Debianscripts/engine/BUILDENV
O runtime C do HQQgo.mod e go.sum

A fixação atual é o Chromium 157.0.8103.38. Um download que não corresponda à sua fixação interrompe o build. A versão aplica exatamente um patch ao Chromium: no Linux, o chrome_net_server é compilado sem NSS e verifica certificados de servidor apenas contra o Chrome Root Store, de modo que o binário não precisa de nada além da libc.

As engines Linux e Windows são compiladas dentro de sandboxes gvx: processos não privilegiados com uma imagem guest fixada e sem daemon no host. As engines Windows são compiladas de forma cruzada no Linux, com o clang-cl e o lld-link do Chromium, contra o Windows SDK que o xwin obtém da Microsoft. As engines macOS são compiladas nativamente em um runner macOS, porque o build do Chromium para macOS exige o Xcode.

Recompilar o mesmo commit produziu binários idênticos byte a byte em execuções de CI separadas. Os binários de release são idênticos byte a byte aos que uma execução anterior de CI compilou para o commit que incorporou a engine ao monorepo, nas seis plataformas.

PlataformaTamanho do binário
linux/amd6427,9 MB
linux/arm6427,4 MB
darwin/amd6426,2 MB
darwin/arm6423,9 MB
windows/amd6427,3 MB
windows/arm6425,8 MB

As engines Linux dependem apenas de libc, libm, libdl, libpthread e libgcc_s, e exigem glibc 2.31 ou mais recente.

Acompanhando o Chromium e as Releases

Um workflow agendado verifica o Chromium Stable a cada seis horas. Para uma nova versão, ele reescreve as fixações, compila e testa as seis engines em uma branch chromium/<versão>, atesta os binários e, em um único commit, grava na main as fixações junto com os binários embutidos, seus digests e os textos de licença. No monorepo, arquivos OWNERS por arquivo concedem ao bot do workflow exatamente esses arquivos, de modo que a atualização é integrada sem intervenção humana; já as mudanças na própria receita de build (a fixação do Windows SDK e o patch) ainda exigem um responsável.

Cada binário de release traz uma atestação de proveniência de build emitida pelo workflow que o compilou:

1gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet

Os textos de licença do que cada binário contém (Chromium, BoringSSL, QUICHE e o restante do grafo de build) são gerados pelo próprio tools/licenses/licenses.py do Chromium e distribuídos com a release como NOTICE_<os>_<arch>.txt.

Limites

  • DialWebSocket é um cliente RFC 6455 em Go puro e não usa a pilha do Chromium.
  • DialWebTransport é uma implementação em processo. Ela não alcança um servidor.
  • As impressões digitais padrão de JA4 e HTTP/2 são as da versão fixada do Chromium. O Chromium as altera de versão para versão, e o chromenet acompanha essas mudanças.
  • Os binários macOS são assinados ad-hoc e não são notarizados.
  • Um processo de trabalho consome memória. Cada binário da engine ocupa de 24 a 28 MB em disco, e o runtime do Chromium inicia um pool de threads e uma thread de rede.
  • Uma sessão da engine é mantida até que o corpo da resposta seja fechado. Um programa que vaza corpos de resposta esgotará o pool.

Fonte

O repositório é github.com/gosuda/chromenet, espelhado de //net/chromenet no monorepo gosuda. A receita de build está em scripts/engine, a integração da engine em engine/, e a interface em chrome_net.idl.