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

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:
| Propriedade | Valor observado |
|---|---|
| Protocolo negociado | h2 |
| JA4 | t13d1516h2_8daaf6152771_806a8c22fdea |
| Impressão digital HTTP/2 (formato Akamai) | 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p |
| User-Agent | Chrome 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.Clientfaz ambos, como ocorre com ohttp.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.NetErrorque 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
| Campo | Efeito |
|---|---|
UserAgent | Substitui o User-Agent padrão do Chrome. Um cabeçalho User-Agent da requisição continua prevalecendo. |
AcceptLanguage | Substitui en-US,en;q=0.9. |
Proxy | Regras de proxy do Chromium, como http://127.0.0.1:8080 ou socks5://127.0.0.1:1080. Vazio significa conexão direta. |
DisableHTTP2, DisableQUIC | Desativam os protocolos. |
ForceHTTP3 | Usa QUIC para todas as origens, sem aguardar um anúncio Alt-Svc. |
InsecureSkipVerify | Aceita qualquer certificado de servidor. |
Pool | Sessõ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
| Camada | O que é executado ali |
|---|---|
| Seu programa | http.Client e chromenet.Transport, em Go puro. |
| Fronteira de processo | RPC 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_server | Uma 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 //net | URLRequestContext, 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ção | Onde |
|---|---|
Versão do Chromium e seu arquivo oficial de código-fonte -lite | third_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, xwin | third_party/chromium/SHA256 |
| Versões do MSVC CRT e do Windows SDK obtidas via xwin | third_party/chromium/WINSDK |
A versão do runtime de sandbox (rungv), o digest da imagem guest, a data do snapshot do Debian | scripts/engine/BUILDENV |
| O runtime C do HQQ | go.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.
| Plataforma | Tamanho do binário |
|---|---|
linux/amd64 | 27,9 MB |
linux/arm64 | 27,4 MB |
darwin/amd64 | 26,2 MB |
darwin/arm64 | 23,9 MB |
windows/amd64 | 27,3 MB |
windows/arm64 | 25,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
chromenetacompanha 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.