gosuda.org/chromenet: Stiva de rețea a Chromium pentru Go, construită reproductibil dintr-o versiune fixată

gosuda.org/chromenet este un http.RoundTripper pentru Go, care trimite cereri prin propria stivă de rețea a Chromium. Stiva este //net dintr-o versiune oficială Chromium: implementările sale HTTP/1.1, HTTP/2 și QUIC/HTTP/3, BoringSSL, configurația TLS a Chrome și Chrome Root Store. Aceasta rulează într-un proces worker separat, chrome_net_server, iar partea Go rămâne Go pur (CGO_ENABLED=0), comunicând cu worker-ul prin RPC cu memorie partajată HQQ.
1go get gosuda.org/chromenet@v0.1.10157081033800
De ce să rulăm stiva de rețea a Chromium
Un program Go care comunică pe web cu net/http îi prezintă serverului un client Go. Acesta este comportamentul implicit corect pentru traficul serviciu-la-serviciu. Este însă greșit atunci când comportamentul serverului depinde de tipul de client pe care îl crede în fața sa: endpoint-uri care impun un browser, middleware anti-bot, CDN-uri care servesc versiuni diferite de protocol unor amprente TLS și HTTP/2 diferite, precum și teste care trebuie să reproducă ceea ce experimentează un browser real.
Biblioteca standard Go nu poate reproduce comportamentul pe fir al unui browser. crypto/tls are propria ordine a suitelor de cifruri și propriul set de extensii, iar net/http are propriile setări HTTP/2. Bibliotecile care imită un browser prin copierea octeților ClientHello ai acestuia continuă să copieze, lansare după lansare. chromenet urmează o altă cale: nu imită stiva de rețea Chromium, ci o rulează.
Măsurat pe motorul darwin/arm64, față de un serviciu public de amprentare (tls.peet.ws), configurația implicită raportează:
| Proprietate | Valoare observată |
|---|---|
| Protocol negociat | h2 |
| JA4 | t13d1516h2_8daaf6152771_806a8c22fdea |
| Amprentă HTTP/2 (format Akamai) | 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p |
| User-Agent | Chrome desktop, de versiunea majoră a motorului, de exemplu Chrome/157.0.0.0 |
Cu ForceHTTP3, cererile către cloudflare-quic.com și www.google.com au primit răspuns prin HTTP/3. Aceste valori provin de pe o singură platformă, dintr-o singură zi. Motorul este chiar stiva, deci valorile se schimbă atunci când se schimbă versiunea Chromium fixată.
Utilizare
NewTransport returnează un http.RoundTripper. Folosiți-l oriunde ați folosi 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}
La prima cerere, modulul scrie binarul worker încorporat pentru platforma dumneavoastră în directorul cache al utilizatorului, verifică digestul său SHA-256 cu gosuda.org/hqq/secureexec, îl pornește cu o legătură de viață cu procesul părinte (--parent-stdin, astfel încât se oprește când se oprește procesul dumneavoastră) și deschide un pool de sesiuni. Nu este nevoie de nicio instalare.
Câteva comportamente urmează http.Transport, astfel încât http.Client să funcționeze neschimbat:
- Transport-ul nu urmează redirecționările și nu păstrează cookie-uri.
http.Clientface ambele lucruri, la fel ca în cazulhttp.Transport. - Răspunsurile comprimate cu gzip, deflate, Brotli sau zstd sosesc decodificate, iar
resp.Uncompressedeste true. - O cerere ține o sesiune a motorului până când corpul răspunsului este închis. Închideți corpurile.
- Anularea contextului cererii anulează cererea în interiorul Chromium.
- Erorile de rețea sunt valori
*client.NetError, care conțin codul și numele erorii Chromium:
1var ne *client.NetError
2if errors.As(err, &ne) {
3 fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
4}
Opțiuni
| Câmp | Efect |
|---|---|
UserAgent | Înlocuiește User-Agent-ul implicit Chrome. Un antet de cerere User-Agent are în continuare prioritate. |
AcceptLanguage | Înlocuiește en-US,en;q=0.9. |
Proxy | Reguli de proxy Chromium, precum http://127.0.0.1:8080 sau socks5://127.0.0.1:1080. Valoarea goală înseamnă conexiune directă. |
DisableHTTP2, DisableQUIC | Dezactivează protocoalele. |
ForceHTTP3 | Folosește QUIC pentru fiecare origine, fără a aștepta un anunț Alt-Svc. |
InsecureSkipVerify | Acceptă orice certificat de server. |
Pool | Sesiunile motorului de utilizat. Nil folosește client.DefaultPool(), care permite 64 de cereri concurente. |
Transporturile cu opțiuni egale partajează un singur Chromium URLRequestContext, astfel încât pool-urile de conexiuni, sesiunile HTTP/2 și sesiunile QUIC sunt reutilizate între cereri. Setarea HQQ_CHROME_NET_SERVER=/path/to/chrome_net_server înlocuiește worker-ul încorporat, de exemplu cu un build local.
Cum funcționează
| Strat | Ce rulează acolo |
|---|---|
| Programul dumneavoastră | http.Client și chromenet.Transport, Go pur. |
| Granița de proces | RPC cu memorie partajată HQQ. Interfața este chrome_net.idl, compilată de hqq-bindgen în clientul Go și în glue-ul serverului C. |
chrome_net_server | Un strat C++ mic peste Chromium: un fir de rețea care deține fiecare obiect //net și fire de sesiune care blochează până când firul de rețea răspunde la o cerere. |
Chromium //net | URLRequestContext, URLRequest, sesiuni HTTP/2 și QUIC, BoringSSL. |
IDL-ul are șase operații esențiale: crearea unui motor din opțiuni key=value, pornirea unei cereri cu antetul său (METHOD URL plus liniile de antet) și lungimea corpului, scrierea fragmentelor din corpul cererii, așteptarea antetului răspunsului, citirea corpului decodificat și distrugerea handle-urilor. Un handle de cerere declară @cancel, astfel încât un apel abandonat întrerupe cererea în interiorul Chromium, în loc să o lase să ruleze până la capăt.
Izolarea stivei într-un proces worker este atât o proprietate de siguranță, cât și una de împachetare. Codul de rețea al Chromium analizează date ostile provenite din internetul deschis. Dacă se blochează, serviciul dumneavoastră Go nu se blochează odată cu el: apelul eșuează cu o eroare, sesiunea este eliminată, iar clientul pornește un worker nou data viitoare când are nevoie de unul.
Construit reproductibil, fixat prin SHA-256
Un binar de 25 MB care conține Chromium este demn de încredere doar dacă puteți spune exact ce a intrat în el. chromenet urmărește Chromium Stable în același mod în care gosuda.org/sqlite3 urmărește SQLite: sursa nu este niciodată vendorizată, doar fixată.
| Fixare | Unde |
|---|---|
Versiunea Chromium și arhiva sa oficială de sursă -lite | third_party/chromium/VERSION |
| SHA-256 al arhivei și al fiecărui toolchain fixat de acea versiune: clang, Rust, GN, ninja, runtime-ul clang pentru Windows, xwin | third_party/chromium/SHA256 |
| Versiunile MSVC CRT și Windows SDK preluate prin xwin | third_party/chromium/WINSDK |
Release-ul runtime-ului sandbox (rungv), digestul imaginii guest, data snapshot-ului Debian | scripts/engine/BUILDENV |
| Runtime-ul C HQQ | go.mod și go.sum |
Fixarea curentă este Chromium 157.0.8103.38. Un download care nu corespunde fixării sale oprește build-ul. Release-ul aplică exact un patch la Chromium: pe Linux, chrome_net_server este construit fără NSS și verifică certificatele serverelor doar față de Chrome Root Store, astfel încât binarul nu are nevoie de nimic în afara libc.
Motoarele Linux și Windows sunt construite în sandbox-uri gvx: procese neprivilegiate, cu o imagine guest fixată și fără daemon pe gazdă. Motoarele Windows sunt compilate încrucișat pe Linux, cu clang-cl și lld-link ale Chromium, împotriva Windows SDK pe care xwin îl preia de la Microsoft. Motoarele macOS se construiesc nativ pe un runner macOS, deoarece build-ul Chromium pentru macOS necesită Xcode.
Reconstruirea aceluiași commit a produs binare identice byte cu byte între rulări CI separate. Binarele de release sunt identice byte cu cele pe care o rulare CI anterioară le-a construit pentru commit-ul care a introdus motorul în monorepo, pe toate cele șase platforme.
| Platformă | Dimensiune binar |
|---|---|
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 |
Motoarele Linux depind doar de libc, libm, libdl, libpthread și libgcc_s și necesită glibc 2.31 sau mai nou.
Urmărirea Chromium și a release-urilor
Un workflow programat verifică Chromium Stable la fiecare șase ore. Pentru un release nou, rescrie fixările, construiește și testează toate cele șase motoare pe un branch chromium/<version>, atestă binarele și comite fixările împreună cu binarele încorporate, digesturile lor și textele licențelor pe main, într-un singur commit. În monorepo, fișierele OWNERS pe fiecare fișier acordă botului workflow-ului exact acele fișiere, astfel încât actualizarea intră fără un pas uman, în timp ce modificările rețetei de build în sine (fixarea Windows SDK și patch-ul) necesită în continuare un owner.
Fiecare binar de release are o atestare de proveniență a build-ului, emisă de workflow-ul care l-a construit:
1gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet
Textele licențelor pentru fiecare componentă din binare (Chromium, BoringSSL, QUICHE și restul graficului de build) sunt generate de tools/licenses/licenses.py propriu al Chromium și livrate cu release-ul ca NOTICE_<os>_<arch>.txt.
Limite
DialWebSocketeste un client RFC 6455 pur Go și nu folosește stiva Chromium.DialWebTransporteste o implementare în proces. Nu ajunge la un server.- Amprentele implicite JA4 și HTTP/2 sunt cele ale versiunii Chromium fixate. Chromium le modifică de la un release la altul, iar
chromenetle urmează. - Binarele macOS sunt semnate ad-hoc și nu sunt notarizate.
- Un proces worker consumă memorie. Fiecare binar de motor are 24 până la 28 MB pe disc, iar runtime-ul Chromium pornește un pool de fire de execuție și un fir de rețea.
- O sesiune a motorului este ținută până când corpul răspunsului este închis. Un program care scurge corpuri de răspuns va epuiza pool-ul.
Sursă
Repository-ul este github.com/gosuda/chromenet, oglindit din //net/chromenet din monorepo-ul gosuda. Rețeta de build se află în scripts/engine, glue-ul motorului în engine/, iar interfața în chrome_net.idl.