GoSuda

gosuda.org/chromenet: lo stack di rete di Chromium per Go, costruito in modo riproducibile a partire da una release fissata

By Lemon Mint views ...

gosuda.org/chromenet è un http.RoundTripper di Go che invia le richieste attraverso lo stack di rete nativo di Chromium. Lo stack è //net di una release ufficiale di Chromium: le sue implementazioni di HTTP/1.1, HTTP/2 e QUIC/HTTP/3, BoringSSL, la configurazione TLS di Chrome e il Chrome Root Store. Viene eseguito in un processo worker separato, chrome_net_server, mentre il lato Go rimane puro Go (CGO_ENABLED=0), e comunica con il worker tramite RPC a memoria condivisa HQQ.

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

Perché eseguire lo stack di rete di Chromium

Un programma Go che comunica con il web tramite net/http si presenta al server come un client Go. È il comportamento predefinito corretto per il traffico tra servizi. È invece sbagliato quando il comportamento del server dipende dal tipo di client che ritiene di avere di fronte: endpoint che richiedono un browser, middleware anti-bot, CDN che servono versioni di protocollo diverse in base alle impronte TLS e HTTP/2, e test che devono riprodurre ciò che un browser reale sperimenta.

La libreria standard di Go non può riprodurre il comportamento a livello di rete di un browser. crypto/tls possiede un proprio ordine delle cipher suite e un proprio insieme di estensioni, mentre net/http ha le proprie impostazioni HTTP/2. Le librerie che imitano un browser copiando i byte del suo ClientHello continuano a doverli copiare, release dopo release. chromenet segue un'altra strada: non imita lo stack di rete di Chromium, ma lo esegue.

Misurato sul motore darwin/arm64 con un servizio pubblico di analisi delle impronte (tls.peet.ws), la configurazione predefinita riporta:

ProprietàValore osservato
Protocollo negoziatoh2
JA4t13d1516h2_8daaf6152771_806a8c22fdea
Impronta HTTP/2 (formato Akamai)1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
User-AgentChrome desktop della versione principale del motore, ad es. Chrome/157.0.0.0

Con ForceHTTP3, le richieste verso cloudflare-quic.com e www.google.com hanno ricevuto risposta tramite HTTP/3. Questi valori provengono da una sola piattaforma in un solo giorno. Il motore è lo stack stesso, quindi cambiano quando cambia la release di Chromium fissata.

Utilizzo

NewTransport restituisce un http.RoundTripper. Lo si può usare ovunque si userebbe 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}

Alla prima richiesta, il modulo scrive il binario worker incorporato per la propria piattaforma nella directory di cache dell'utente, verifica il suo digest SHA-256 con gosuda.org/hqq/secureexec, lo avvia con un collegamento al processo padre (--parent-stdin, in modo che termini quando termina il processo chiamante) e apre un pool di sessioni. Non è necessario installare nulla.

Alcuni comportamenti seguono quelli di http.Transport affinché http.Client funzioni senza modifiche:

  • Il Transport non segue i reindirizzamenti e non conserva cookie. http.Client esegue entrambe le operazioni, come avviene con http.Transport.
  • Le risposte compresse con gzip, deflate, Brotli o zstd arrivano già decodificate, e resp.Uncompressed è impostato su true.
  • Una richiesta mantiene una sessione del motore fino alla chiusura del corpo della risposta. Chiudere sempre i corpi.
  • L'annullamento del contesto della richiesta annulla la richiesta anche all'interno di Chromium.
  • Gli errori di rete sono valori *client.NetError che contengono il codice e il nome dell'errore di Chromium:
1var ne *client.NetError
2if errors.As(err, &ne) {
3	fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
4}

Opzioni

CampoEffetto
UserAgentSostituisce lo User-Agent predefinito di Chrome. Un header User-Agent della richiesta ha comunque la precedenza.
AcceptLanguageSostituisce en-US,en;q=0.9.
ProxyRegole proxy di Chromium, come http://127.0.0.1:8080 o socks5://127.0.0.1:1080. Se vuoto, la connessione è diretta.
DisableHTTP2, DisableQUICDisattivano i rispettivi protocolli.
ForceHTTP3Usa QUIC per ogni origine senza attendere un annuncio Alt-Svc.
InsecureSkipVerifyAccetta qualsiasi certificato del server.
PoolSessioni del motore da utilizzare. Se nil, viene usato client.DefaultPool(), che consente 64 richieste concorrenti.

I Transport con opzioni uguali condividono un unico URLRequestContext di Chromium, quindi i pool di connessioni, le sessioni HTTP/2 e le sessioni QUIC sono riutilizzati tra le richieste. Impostando HQQ_CHROME_NET_SERVER=/path/to/chrome_net_server si sostituisce il worker incorporato, ad esempio con una build locale.

Come funziona

LivelloCosa viene eseguito
Il tuo programmahttp.Client e chromenet.Transport, puro Go.
Confine di processoRPC a memoria condivisa HQQ. L'interfaccia è chrome_net.idl, compilata da hqq-bindgen nel client Go e nel codice di collegamento del server C.
chrome_net_serverUn piccolo livello C++ sopra Chromium: un thread di rete che possiede tutti gli oggetti di //net, e thread di sessione che si bloccano su una richiesta finché il thread di rete non risponde.
Chromium //netURLRequestContext, URLRequest, sessioni HTTP/2 e QUIC, BoringSSL.

L'IDL prevede sei operazioni fondamentali: creare un motore a partire da opzioni key=value, avviare una richiesta con la sua intestazione (METHOD URL più le righe di header) e la lunghezza del corpo, scrivere i blocchi del corpo della richiesta, attendere l'intestazione della risposta, leggere il corpo decodificato e distruggere gli handle. Un handle di richiesta dichiara @cancel, quindi una chiamata abbandonata interrompe la richiesta all'interno di Chromium invece di lasciarla proseguire fino al completamento.

Isolare lo stack in un processo worker è una proprietà di sicurezza oltre che di packaging. Il codice di rete di Chromium elabora input ostili provenienti da internet aperta. Se va in crash, il tuo servizio Go non va in crash con esso: la chiamata fallisce con un errore, la sessione viene scartata e il client avvia un nuovo worker la volta successiva in cui ne ha bisogno.

Build riproducibile, fissata tramite SHA-256

Un binario di 25 MB che contiene Chromium è affidabile solo se è possibile dichiarare con precisione cosa vi è stato incluso. chromenet segue Chromium Stable nello stesso modo in cui gosuda.org/sqlite3 segue SQLite: il sorgente non viene mai incluso nel repository (vendoring), ma soltanto fissato tramite pin.

PinDove
Release di Chromium e relativo archivio sorgente ufficiale -litethird_party/chromium/VERSION
SHA-256 dell'archivio e di ogni toolchain fissata da quella release: clang, Rust, GN, ninja, il runtime clang per Windows, xwinthird_party/chromium/SHA256
Versioni di MSVC CRT e Windows SDK ottenute tramite xwinthird_party/chromium/WINSDK
La release del runtime sandbox (rungv), il digest dell'immagine guest, la data dello snapshot Debianscripts/engine/BUILDENV
Il runtime C di HQQgo.mod e go.sum

Il pin attuale è Chromium 157.0.8103.38. Un download che non corrisponde al proprio pin interrompe la build. La release applica esattamente una patch a Chromium: su Linux, chrome_net_server viene compilato senza NSS e verifica i certificati dei server esclusivamente con il Chrome Root Store, in modo che il binario non richieda nulla oltre alla libc.

I motori Linux e Windows vengono compilati all'interno di sandbox gvx: processi senza privilegi con un'immagine guest fissata e nessun daemon sull'host. I motori Windows vengono compilati in cross-compilazione su Linux con clang-cl e lld-link di Chromium, con il Windows SDK che xwin scarica da Microsoft. I motori macOS vengono compilati in modo nativo su un runner macOS, perché la build di Chromium per macOS richiede Xcode.

Rieseguire la build dello stesso commit ha prodotto binari identici byte per byte in esecuzioni CI separate. I binari di release sono identici byte per byte a quelli che una precedente esecuzione CI aveva compilato per il commit con cui il motore è entrato nel monorepo, su tutte e sei le piattaforme.

PiattaformaDimensione del binario
linux/amd6427.9 MB
linux/arm6427.4 MB
darwin/amd6426.2 MB
darwin/arm6423.9 MB
windows/amd6427.3 MB
windows/arm6425.8 MB

I motori Linux dipendono solo da libc, libm, libdl, libpthread e libgcc_s, e richiedono glibc 2.31 o successiva.

Monitoraggio di Chromium e delle release

Un workflow pianificato controlla Chromium Stable ogni sei ore. In caso di nuova release, riscrive i pin, compila e testa tutti e sei i motori su un branch chromium/<version>, certifica i binari e include in un unico commit su main i pin insieme ai binari incorporati, ai loro digest e ai testi delle licenze. Nel monorepo, i file OWNERS per file concedono al bot del workflow esattamente questi file, in modo che l'aggiornamento venga integrato senza un intervento umano, mentre le modifiche alla ricetta di build stessa (il pin del Windows SDK e la patch) richiedono ancora un responsabile.

Ogni binario di release reca un'attestazione di provenienza della build, emessa dal workflow che lo ha compilato:

1gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet

I testi delle licenze di ciò che ogni binario contiene (Chromium, BoringSSL, QUICHE e il resto del grafo di build) sono generati dallo script tools/licenses/licenses.py di Chromium e distribuiti con la release come NOTICE_<os>_<arch>.txt.

Limiti

  • DialWebSocket è un client puro Go conforme a RFC 6455 e non utilizza lo stack di Chromium.
  • DialWebTransport è un'implementazione in-process. Non raggiunge un server.
  • Le impronte JA4 e HTTP/2 predefinite sono quelle della release di Chromium fissata. Chromium le modifica da una release all'altra, e chromenet le segue.
  • I binari macOS sono firmati ad-hoc e non sono notarizzati.
  • Un processo worker comporta un costo in memoria. Ogni binario del motore occupa da 24 a 28 MB su disco, e il runtime di Chromium avvia un pool di thread e un thread di rete.
  • Una sessione del motore resta occupata fino alla chiusura del corpo della risposta. Un programma che non chiude i corpi delle risposte esaurirà il pool.

Sorgente

Il repository è github.com/gosuda/chromenet, replicato da //net/chromenet nel monorepo di gosuda. La ricetta di build si trova in scripts/engine, il codice di collegamento del motore in engine/, e l'interfaccia in chrome_net.idl.