gosuda.org/chromenet: Chromiums Netzwerk-Stack für Go, reproduzierbar aus einem fixierten Release erstellt

gosuda.org/chromenet ist ein Go-http.RoundTripper, der Anfragen über den eigenen Netzwerk-Stack von Chromium sendet. Dieser Stack ist //net aus einer offiziellen Chromium-Version: deren HTTP/1.1-, HTTP/2- und QUIC/HTTP/3-Implementierungen, BoringSSL, die TLS-Konfiguration von Chrome und der Chrome Root Store. Er läuft in einem separaten Worker-Prozess, chrome_net_server, während die Go-Seite reines Go bleibt (CGO_ENABLED=0) und über HQQ Shared-Memory-RPC mit dem Worker kommuniziert.
1go get gosuda.org/chromenet@v0.1.10157081033800
Warum Chromiums Netzwerk-Stack ausführen
Ein Go-Programm, das mit net/http über das Web kommuniziert, tritt gegenüber dem Server als Go-Client auf. Das ist für die Kommunikation zwischen Diensten der richtige Standard. Es ist jedoch die falsche Wahl, wenn das Verhalten des Servers davon abhängt, für welche Art von Client er sein Gegenüber hält: browsergesteuerte Endpunkte, Anti-Bot-Middleware, CDNs, die unterschiedlichen TLS- und HTTP/2-Fingerprints unterschiedliche Protokollversionen ausliefern, sowie Tests, die exakt das Erlebnis eines echten Browsers reproduzieren müssen.
Die Standardbibliothek von Go kann das Wire-Verhalten eines Browsers nicht abbilden. crypto/tls besitzt eine eigene Reihenfolge der Cipher-Suites und einen eigenen Satz an Erweiterungen, und net/http verwendet eigene HTTP/2-Einstellungen. Bibliotheken, die einen Browser imitieren, indem sie dessen ClientHello-Bytes kopieren, müssen dies mit jeder Version erneut tun. chromenet geht den anderen Weg: Es imitiert den Chromium-Netzwerk-Stack nicht, sondern führt ihn tatsächlich aus.
Gemessen auf der darwin/arm64-Engine gegen einen öffentlichen Fingerprint-Dienst (tls.peet.ws) meldet die Standardkonfiguration:
| Eigenschaft | Beobachteter Wert |
|---|---|
| Ausgehandeltes Protokoll | h2 |
| JA4 | t13d1516h2_8daaf6152771_806a8c22fdea |
| HTTP/2-Fingerprint (Akamai-Format) | 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p |
| User-Agent | Desktop-Chrome in der Hauptversion der Engine, z. B. Chrome/157.0.0.0 |
Mit ForceHTTP3 wurden Anfragen an cloudflare-quic.com und www.google.com über HTTP/3 beantwortet. Diese Werte stammen von einer Plattform an einem einzelnen Tag. Die Engine ist der Stack, daher ändern sich die Werte, wenn sich die gepinnte Chromium-Version ändert.
Verwendung
NewTransport gibt einen http.RoundTripper zurück. Verwenden Sie ihn überall dort, wo Sie http.Transport verwenden würden:
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}
Bei der ersten Anfrage schreibt das Modul das eingebettete Worker-Binary für Ihre Plattform in das Benutzer-Cache-Verzeichnis, prüft dessen SHA-256-Digest mit gosuda.org/hqq/secureexec, startet es mit einer Lebensleine zum Elternprozess (--parent-stdin, sodass es beendet wird, wenn Ihr Prozess endet) und öffnet einen Session-Pool. Es muss nichts installiert werden.
Einige Verhaltensweisen entsprechen http.Transport, damit http.Client unverändert funktioniert:
- Der Transport folgt keinen Weiterleitungen und speichert keine Cookies.
http.Clienttut beides, wie auch beihttp.Transport. - Mit gzip, deflate, Brotli oder zstd komprimierte Antworten werden dekodiert übermittelt, und
resp.Uncompressedisttrue. - Eine Anfrage belegt eine Engine-Session, bis der Response-Body geschlossen wird. Schließen Sie die Bodys.
- Das Abbrechen des Anfragekontexts bricht die Anfrage innerhalb von Chromium ab.
- Netzwerkfehler sind
*client.NetError-Werte, die den Chromium-Fehlercode und -namen enthalten:
1var ne *client.NetError
2if errors.As(err, &ne) {
3 fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
4}
Optionen
| Feld | Wirkung |
|---|---|
UserAgent | Ersetzt den standardmäßigen Chrome-User-Agent. Ein User-Agent-Anfrageheader hat weiterhin Vorrang. |
AcceptLanguage | Ersetzt en-US,en;q=0.9. |
Proxy | Chromium-Proxy-Regeln wie http://127.0.0.1:8080 oder socks5://127.0.0.1:1080. Leer bedeutet direkte Verbindung. |
DisableHTTP2, DisableQUIC | Schalten die Protokolle ab. |
ForceHTTP3 | Verwendet QUIC für jeden Ursprung, ohne auf eine Alt-Svc-Ankündigung zu warten. |
InsecureSkipVerify | Akzeptiert jedes Serverzertifikat. |
Pool | Zu verwendende Engine-Sessions. Nil verwendet client.DefaultPool(), das 64 gleichzeitige Anfragen erlaubt. |
Transports mit gleichen Optionen teilen sich einen Chromium-URLRequestContext, sodass Verbindungspools, HTTP/2-Sessions und QUIC-Sessions über Anfragen hinweg wiederverwendet werden. Mit HQQ_CHROME_NET_SERVER=/path/to/chrome_net_server wird der eingebettete Worker ersetzt, etwa durch einen lokalen Build.
Wie es funktioniert
| Schicht | Was dort läuft |
|---|---|
| Ihr Programm | http.Client und chromenet.Transport, reines Go. |
| Prozessgrenze | HQQ-Shared-Memory-RPC. Die Schnittstelle ist chrome_net.idl, kompiliert von hqq-bindgen in den Go-Client und die C-Server-Glue. |
chrome_net_server | Eine kleine C++-Schicht auf Chromium: ein Netzwerk-Thread, der jedes //net-Objekt besitzt, sowie Session-Threads, die bis zur Antwort des Netzwerk-Threads auf eine Anfrage blockieren. |
Chromium //net | URLRequestContext, URLRequest, HTTP/2- und QUIC-Sessions, BoringSSL. |
Die IDL umfasst sechs relevante Operationen: Erstellen einer Engine aus key=value-Optionen, Starten einer Anfrage mit ihrem Kopf (METHOD URL plus Headerzeilen) und der Body-Länge, Schreiben von Request-Body-Chunks, Warten auf den Antwortkopf, Lesen des dekodierten Bodys sowie Zerstören der Handles. Ein Anfrage-Handle deklariert @cancel, sodass ein verworfener Aufruf die Anfrage innerhalb von Chromium unterbricht, anstatt sie bis zum Ende laufen zu lassen.
Die Isolation des Stacks in einem Worker-Prozess ist ebenso eine Sicherheitseigenschaft wie eine Frage der Paketierung. Der Netzwerkcode von Chromium verarbeitet feindliche Eingaben aus dem offenen Internet. Stürzt er ab, stürzt Ihr Go-Dienst nicht mit ab: Der Aufruf schlägt mit einem Fehler fehl, die Session wird verworfen, und der Client startet beim nächsten Bedarf einen neuen Worker.
Reproduzierbar gebaut, per SHA-256 gepinnt
Ein 25 MB großes Binary, das Chromium enthält, ist nur dann vertrauenswürdig, wenn Sie genau angeben können, was hineingeflossen ist. chromenet folgt Chromium Stable so, wie gosuda.org/sqlite3 SQLite folgt: Der Quellcode wird nie vendored, sondern ausschließlich gepinnt.
| Pin | Ort |
|---|---|
Chromium-Version und ihr offizielles -lite-Quellarchiv | third_party/chromium/VERSION |
| SHA-256 des Archivs und aller Toolchains, die diese Version festlegt: clang, Rust, GN, ninja, die clang-Laufzeit für Windows, xwin | third_party/chromium/SHA256 |
| MSVC-CRT- und Windows-SDK-Versionen, die über xwin bezogen werden | third_party/chromium/WINSDK |
Die Version der Sandbox-Laufzeit (rungv), der Digest des Gast-Images, das Debian-Snapshot-Datum | scripts/engine/BUILDENV |
| Die HQQ-C-Laufzeit | go.mod und go.sum |
Der aktuelle Pin ist Chromium 157.0.8103.38. Ein Download, der nicht mit seinem Pin übereinstimmt, bricht den Build ab. Das Release wendet genau einen Patch auf Chromium an: Unter Linux wird chrome_net_server ohne NSS gebaut und prüft Serverzertifikate ausschließlich gegen den Chrome Root Store, sodass das Binary nichts außer libc benötigt.
Die Linux- und Windows-Engines werden in gvx-Sandboxes gebaut: unprivilegierte Prozesse mit einem gepinnten Gast-Image und ohne Host-Daemon. Die Windows-Engines werden unter Linux mit clang-cl und lld-link von Chromium gegen das Windows SDK cross-kompiliert, das xwin von Microsoft bezieht. Die macOS-Engines werden nativ auf einem macOS-Runner gebaut, weil der macOS-Build von Chromium Xcode benötigt.
Das erneute Bauen desselben Commits erzeugte in getrennten CI-Läufen byte-identische Binaries. Die Release-Binaries sind auf allen sechs Plattformen byte-identisch mit denjenigen, die ein früherer CI-Lauf für den Commit erstellte, der die Engine im Monorepo einführte.
| Plattform | Binärgröße |
|---|---|
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 |
Die Linux-Engines hängen nur von libc, libm, libdl, libpthread und libgcc_s ab und benötigen glibc 2.31 oder neuer.
Verfolgung von Chromium und Releases
Ein geplanter Workflow prüft alle sechs Stunden Chromium Stable. Bei einem neuen Release schreibt er die Pins neu, baut und testet alle sechs Engines auf einem Branch chromium/<version>, attestiert die Binaries und committet die Pins zusammen mit den eingebetteten Binaries, deren Digests und den Lizenztexten in einem einzigen Commit auf main. Im Monorepo gewähren pro Datei gültige OWNERS dem Bot des Workflows genau diese Dateien, sodass das Update ohne menschlichen Schritt einfließt. Änderungen am Build-Rezept selbst (der Windows-SDK-Pin und der Patch) benötigen weiterhin einen Owner.
Jedes Release-Binary trägt eine Build-Provenance-Attestation des Workflows, der es gebaut hat:
1gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet
Die Lizenztexte aller Bestandteile der einzelnen Binaries (Chromium, BoringSSL, QUICHE und der übrige Build-Graph) werden von Chromiums eigenem tools/licenses/licenses.py erzeugt und zusammen mit dem Release als NOTICE_<os>_<arch>.txt ausgeliefert.
Grenzen
DialWebSocketist ein reiner Go-Client nach RFC 6455 und verwendet nicht den Chromium-Stack.DialWebTransportist eine In-Process-Implementierung. Sie erreicht keinen Server.- Die standardmäßigen JA4- und HTTP/2-Fingerprints sind die der gepinnten Chromium-Version. Chromium ändert sie von Release zu Release, und
chromenetfolgt. - Die macOS-Binaries sind ad hoc signiert und nicht notarisiert.
- Ein Worker-Prozess kostet Speicher. Jedes Engine-Binary ist 24 bis 28 MB groß auf dem Datenträger, und die Laufzeit von Chromium startet einen Thread-Pool und einen Netzwerk-Thread.
- Eine Engine-Session wird bis zum Schließen des Response-Bodys gehalten. Ein Programm, das Response-Bodys nicht schließt, erschöpft den Pool.
Quelle
Das Repository ist github.com/gosuda/chromenet und wird aus //net/chromenet im gosuda-Monorepo gespiegelt. Das Build-Rezept liegt in scripts/engine, die Engine-Glue in engine/ und die Schnittstelle in chrome_net.idl.