GoSuda

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

By Lemon Mint views ...

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ă:

ProprietateValoare observată
Protocol negociath2
JA4t13d1516h2_8daaf6152771_806a8c22fdea
Amprentă HTTP/2 (format Akamai)1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
User-AgentChrome 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.Client face ambele lucruri, la fel ca în cazul http.Transport.
  • Răspunsurile comprimate cu gzip, deflate, Brotli sau zstd sosesc decodificate, iar resp.Uncompressed este 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âmpEfect
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.
ProxyReguli de proxy Chromium, precum http://127.0.0.1:8080 sau socks5://127.0.0.1:1080. Valoarea goală înseamnă conexiune directă.
DisableHTTP2, DisableQUICDezactivează protocoalele.
ForceHTTP3Folosește QUIC pentru fiecare origine, fără a aștepta un anunț Alt-Svc.
InsecureSkipVerifyAcceptă orice certificat de server.
PoolSesiunile 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ă

StratCe rulează acolo
Programul dumneavoastrăhttp.Client și chromenet.Transport, Go pur.
Granița de procesRPC 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_serverUn 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 //netURLRequestContext, 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ă.

FixareUnde
Versiunea Chromium și arhiva sa oficială de sursă -litethird_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, xwinthird_party/chromium/SHA256
Versiunile MSVC CRT și Windows SDK preluate prin xwinthird_party/chromium/WINSDK
Release-ul runtime-ului sandbox (rungv), digestul imaginii guest, data snapshot-ului Debianscripts/engine/BUILDENV
Runtime-ul C HQQgo.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/amd6427.9 MB
linux/arm6427.4 MB
darwin/amd6426.2 MB
darwin/arm6423.9 MB
windows/amd6427.3 MB
windows/arm6425.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

  • DialWebSocket este un client RFC 6455 pur Go și nu folosește stiva Chromium.
  • DialWebTransport este 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 chromenet le 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.