gosuda.org/chromenet: Chromiums netwerkstack voor Go, reproduceerbaar gebouwd vanuit een vastgezette release

gosuda.org/chromenet is een Go http.RoundTripper die verzoeken verstuurt via de eigen netwerkstack van Chromium. De stack is //net uit een officiële Chromium-release: de HTTP/1.1-, HTTP/2- en QUIC/HTTP/3-implementaties, BoringSSL, de TLS-configuratie van Chrome en de Chrome Root Store. Deze draait in een apart workerproces, chrome_net_server, en de Go-kant blijft pure Go (CGO_ENABLED=0), die met de worker communiceert via HQQ shared-memory RPC.
1go get gosuda.org/chromenet@v0.1.10157081033800
Waarom de netwerkstack van Chromium draaien
Een Go-programma dat via net/http met het web communiceert, presenteert zich aan de server als een Go-client. Dat is de juiste standaard voor service-naar-service-verkeer. Het is de verkeerde keuze wanneer het gedrag van de server afhangt van wat voor soort client de server denkt te zien: browsergebonden endpoints, anti-botmiddleware, CDN's die verschillende protocolversies serveren aan verschillende TLS- en HTTP/2-fingerprints, en tests die precies moeten reproduceren wat een echte browser ervaart.
De standaardbibliotheek van Go kan het netwerkgedrag van een browser op de draad niet nabootsen. crypto/tls heeft een eigen volgorde van cipher suites en een eigen extensieset, en net/http heeft eigen HTTP/2-instellingen. Bibliotheken die een browser imiteren door de ClientHello-bytes te kopiëren, blijven kopiëren, release na release. chromenet kiest een andere route: het imiteert de Chromium-netwerkstack niet, maar draait haar.
Gemeten op de darwin/arm64-engine tegen een publieke fingerprintdienst (tls.peet.ws), rapporteert de standaardconfiguratie:
| Eigenschap | Geobserveerde waarde |
|---|---|
| Onderhandeld protocol | h2 |
| JA4 | t13d1516h2_8daaf6152771_806a8c22fdea |
| HTTP/2-fingerprint (Akamai-formaat) | 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p |
| User-Agent | desktop Chrome van de hoofdversie van de engine, bijvoorbeeld Chrome/157.0.0.0 |
Met ForceHTTP3 werden verzoeken naar cloudflare-quic.com en www.google.com beantwoord via HTTP/3. Deze cijfers komen van één platform op één dag. De engine is de stack, dus ze veranderen wanneer de vastgepinde Chromium-release verandert.
Gebruik
NewTransport retourneert een http.RoundTripper. Gebruik deze waar je anders http.Transport zou gebruiken:
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}
Bij het eerste verzoek schrijft de module het ingebedde workerbinaire bestand voor jouw platform naar de gebruikerscachemap, verifieert de SHA-256-digest met gosuda.org/hqq/secureexec, start het met een ouderlijke levenslijn (--parent-stdin, zodat het afsluit wanneer je proces afsluit), en opent een sessiepool. Er hoeft niets geïnstalleerd te worden.
Enkele gedragingen volgen http.Transport, zodat http.Client ongewijzigd werkt:
- De Transport volgt geen redirects en bewaart geen cookies.
http.Clientdoet beide, zoals bijhttp.Transport. - Responses die met gzip, deflate, Brotli of zstd zijn gecomprimeerd, komen gedecodeerd aan, en
resp.Uncompressedis true. - Een verzoek houdt één engine-sessie vast totdat de responsebody is gesloten. Sluit bodies.
- Het annuleren van de requestcontext annuleert het verzoek binnen Chromium.
- Netwerkfouten zijn
*client.NetError-waarden die de Chromium-foutcode en -naam bevatten:
1var ne *client.NetError
2if errors.As(err, &ne) {
3 fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
4}
Opties
| Veld | Effect |
|---|---|
UserAgent | Vervangt de standaard Chrome User-Agent. Een User-Agent-requestheader wint nog steeds. |
AcceptLanguage | Vervangt en-US,en;q=0.9. |
Proxy | Chromium-proxyregels zoals http://127.0.0.1:8080 of socks5://127.0.0.1:1080. Leeg betekent direct. |
DisableHTTP2, DisableQUIC | Schakelen de protocollen uit. |
ForceHTTP3 | Gebruikt QUIC voor elke origin zonder te wachten op een Alt-Svc-advertentie. |
InsecureSkipVerify | Accepteert elk servercertificaat. |
Pool | Te gebruiken engine-sessies. Nil gebruikt client.DefaultPool(), dat 64 gelijktijdige verzoeken toestaat. |
Transports met gelijke opties delen één Chromium URLRequestContext, zodat connectiepools, HTTP/2-sessies en QUIC-sessies tussen verzoeken worden hergebruikt. Het instellen van HQQ_CHROME_NET_SERVER=/path/to/chrome_net_server vervangt de ingebedde worker, bijvoorbeeld door een lokale build.
Hoe het werkt
| Laag | Wat daar draait |
|---|---|
| Je programma | http.Client en chromenet.Transport, pure Go. |
| Procesgrens | HQQ shared-memory RPC. De interface is chrome_net.idl, gecompileerd door hqq-bindgen naar de Go-client en de C-serverglue. |
chrome_net_server | Een kleine C++-laag bovenop Chromium: één netwerkthread die elk //net-object bezit, en sessiethreads die blokkeren op een verzoek totdat de netwerkthread antwoordt. |
Chromium //net | URLRequestContext, URLRequest, HTTP/2- en QUIC-sessies, BoringSSL. |
De IDL heeft zes operaties die ertoe doen: een engine maken uit key=value-opties, een verzoek starten met zijn head (METHOD URL plus headerregels) en bodylengte, requestbody-chunks schrijven, wachten op de responsehead, de gedecodeerde body lezen, en de handles vernietigen. Een requesthandle declareert @cancel, zodat een verlaten aanroep het verzoek binnen Chromium onderbreekt in plaats van het tot voltooiing te laten lopen.
Het isoleren van de stack in een workerproces is evenzeer een veiligheidseigenschap als een verpakkingskwestie. De netwerkcode van Chromium parseert vijandige invoer van het open internet. Als die crasht, crasht je Go-service niet mee: de aanroep faalt met een fout, de sessie wordt weggegooid, en de client start de volgende keer dat hij die nodig heeft een nieuwe worker.
Reproduceerbaar gebouwd, vastgepind met SHA-256
Een binair bestand van 25 MB dat Chromium bevat, is alleen betrouwbaar als je precies kunt zeggen wat erin is gegaan. chromenet volgt Chromium Stable op dezelfde manier als gosuda.org/sqlite3 SQLite volgt: de broncode wordt nooit gevendored, alleen vastgepind.
| Pin | Waar |
|---|---|
Chromium-release en het officiële -lite-bronarchief | third_party/chromium/VERSION |
| SHA-256 van het archief en van elke toolchain die die release vastlegt: clang, Rust, GN, ninja, de Windows clang-runtime, xwin | third_party/chromium/SHA256 |
| MSVC CRT- en Windows SDK-versies die via xwin worden opgehaald | third_party/chromium/WINSDK |
De release van de sandbox-runtime (rungv), de digest van het gastimage, de Debian-snapshotdatum | scripts/engine/BUILDENV |
| De HQQ C-runtime | go.mod en go.sum |
De huidige pin is Chromium 157.0.8103.38. Een download die niet overeenkomt met zijn pin, stopt de build. De release past precies één patch toe op Chromium: op Linux wordt chrome_net_server gebouwd zonder NSS en worden servercertificaten alleen geverifieerd tegen de Chrome Root Store, zodat de binary niets anders nodig heeft dan libc.
De Linux- en Windows-engines worden gebouwd binnen gvx-sandboxes: niet-geprivilegieerde processen met een vastgepind gastimage en zonder host-daemon. De Windows-engines worden op Linux cross-gecompileerd met clang-cl en lld-link van Chromium, tegen het Windows SDK dat xwin van Microsoft ophaalt. De macOS-engines worden native gebouwd op een macOS-runner, omdat de macOS-build van Chromium Xcode nodig heeft.
Het opnieuw bouwen van dezelfde commit leverde byte-identieke binaries op over afzonderlijke CI-runs. De releasebinaries zijn byte-identiek aan die welke een eerdere CI-run bouwde voor de commit die de engine in de monorepo introduceerde, op alle zes platforms.
| Platform | Binaire grootte |
|---|---|
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 |
De Linux-engines hangen alleen af van libc, libm, libdl, libpthread en libgcc_s, en hebben glibc 2.31 of nieuwer nodig.
Chromium en releases volgen
Een ingeplande workflow controleert elke zes uur op Chromium Stable. Bij een nieuwe release herschrijft hij de pins, bouwt en test hij alle zes engines op een chromium/<version>-branch, attesteert hij de binaries en commit hij de pins samen met de ingebedde binaries, hun digests en de licentieteksten naar main in één commit. In de monorepo geven per-bestand-OWNERS de bot van de workflow precies die bestanden, zodat de update zonder menselijke stap landt, terwijl wijzigingen aan het buildrecept zelf (de Windows SDK-pin en de patch) nog steeds een eigenaar nodig hebben.
Elke releasebinary draagt een build provenance attestation van de workflow die hem bouwde:
1gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet
De licentieteksten van wat elke binary bevat (Chromium, BoringSSL, QUICHE en de rest van de buildgraaf) worden gegenereerd door tools/licenses/licenses.py van Chromium zelf en met de release meegeleverd als NOTICE_<os>_<arch>.txt.
Beperkingen
DialWebSocketis een pure-Go RFC 6455-client en gebruikt de Chromium-stack niet.DialWebTransportis een in-process implementatie. Het bereikt geen server.- De standaard JA4- en HTTP/2-fingerprints zijn die van de vastgepinde Chromium-release. Chromium wijzigt ze van release tot release, en
chromenetvolgt dat. - De macOS-binaries zijn ad-hoc gesigneerd en niet genotariseerd.
- Een workerproces kost geheugen. Elke engine-binary is 24 tot 28 MB op schijf, en de runtime van Chromium start een threadpool en een netwerkthread.
- Een engine-sessie wordt vastgehouden totdat de responsebody is gesloten. Een programma dat responsebodies lekt, put de pool uit.
Bron
De repository is github.com/gosuda/chromenet, gespiegeld vanuit //net/chromenet in de gosuda-monorepo. Het buildrecept staat in scripts/engine, de engineglue in engine/, en de interface in chrome_net.idl.