GoSuda

gosuda.org/chromenet: Stos sieciowy Chromium dla języka Go, zbudowany w sposób odtwarzalny z przypiętego wydania

By Lemon Mint views ...

gosuda.org/chromenet to http.RoundTripper w języku Go, który wysyła żądania przez własny stos sieciowy Chromium. Stosem jest //net z oficjalnego wydania Chromium: jego implementacje HTTP/1.1, HTTP/2 i QUIC/HTTP/3, BoringSSL, konfiguracja TLS Chrome oraz Chrome Root Store. Działa w osobnym procesie roboczym, chrome_net_server, a strona Go pozostaje czystym Go (CGO_ENABLED=0), komunikując się z procesem roboczym przez RPC HQQ oparte na pamięci współdzielonej.

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

Dlaczego uruchamiać stos sieciowy Chromium

Program w Go, który komunikuje się z siecią za pomocą net/http, przedstawia się serwerowi jako klient Go. Jest to właściwe domyślne zachowanie w ruchu między usługami. Jest ono niewłaściwe wtedy, gdy zachowanie serwera zależy od tego, za jakiego klienta go uważa: punkty końcowe wymagające przeglądarki, oprogramowanie pośredniczące przeciwdziałające botom, sieci CDN serwujące różne wersje protokołów w zależności od odcisków TLS i HTTP/2 oraz testy, które muszą odtwarzać to, czego doświadcza prawdziwa przeglądarka.

Standardowa biblioteka Go nie może odtworzyć zachowania sieciowego przeglądarki. crypto/tls ma własną kolejność zestawów szyfrów i zbiór rozszerzeń, a net/http własne ustawienia HTTP/2. Biblioteki imitujące przeglądarkę przez kopiowanie bajtów ClientHello kopiują je dalej, wydanie po wydaniu. chromenet idzie inną drogą: nie imituje stosu sieciowego Chromium, lecz go uruchamia.

Pomiary na silniku darwin/arm64 względem publicznego serwisu identyfikacji odcisków (tls.peet.ws) dla domyślnej konfiguracji wykazały:

WłaściwośćZaobserwowana wartość
Wynegocjowany protokółh2
JA4t13d1516h2_8daaf6152771_806a8c22fdea
Odcisk HTTP/2 (format Akamai)1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
User-Agentpulpitowy Chrome w głównej wersji silnika, np. Chrome/157.0.0.0

Przy użyciu ForceHTTP3 żądania do cloudflare-quic.com i www.google.com otrzymały odpowiedzi przez HTTP/3. Liczby pochodzą z jednej platformy z jednego dnia. Silnik jest stosem, więc zmieniają się one wraz ze zmianą przypiętego wydania Chromium.

Użycie

NewTransport zwraca http.RoundTripper. Używaj go wszędzie tam, gdzie użyłbyś 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}

Przy pierwszym żądaniu moduł zapisuje osadzony plik binarny procesu roboczego dla Twojej platformy do katalogu cache użytkownika, weryfikuje jego skrót SHA-256 za pomocą gosuda.org/hqq/secureexec, uruchamia go z łączem nadrzędnym (--parent-stdin, dzięki czemu kończy działanie wraz z Twoim procesem) i otwiera pulę sesji. Nie trzeba niczego instalować.

Kilka zachowań jest zgodnych z http.Transport, dzięki czemu http.Client działa bez zmian:

  • Transport nie podąża za przekierowaniami i nie przechowuje ciasteczek. Robi to http.Client, tak jak w przypadku http.Transport.
  • Odpowiedzi skompresowane przez gzip, deflate, Brotli lub zstd docierają już zdekodowane, a resp.Uncompressed ma wartość true.
  • Żądanie zajmuje jedną sesję silnika do momentu zamknięcia ciała odpowiedzi. Zamykaj ciała odpowiedzi.
  • Anulowanie kontekstu żądania anuluje żądanie wewnątrz Chromium.
  • Błędy sieciowe to wartości *client.NetError zawierające kod i nazwę błędu Chromium:
1var ne *client.NetError
2if errors.As(err, &ne) {
3	fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
4}

Opcje

PoleDziałanie
UserAgentZastępuje domyślny User-Agent Chrome. Nagłówek żądania User-Agent nadal ma pierwszeństwo.
AcceptLanguageZastępuje en-US,en;q=0.9.
ProxyReguły proxy Chromium, np. http://127.0.0.1:8080 lub socks5://127.0.0.1:1080. Pusta wartość oznacza połączenie bezpośrednie.
DisableHTTP2, DisableQUICWyłączają te protokoły.
ForceHTTP3Używa QUIC dla każdego originu, bez czekania na ogłoszenie Alt-Svc.
InsecureSkipVerifyAkceptuje dowolny certyfikat serwera.
PoolSesje silnika do użycia. Wartość nil oznacza client.DefaultPool(), który pozwala na 64 równoległe żądania.

Transporty z równymi opcjami współdzielą jeden URLRequestContext Chromium, więc pule połączeń, sesje HTTP/2 i sesje QUIC są ponownie używane między żądaniami. Ustawienie HQQ_CHROME_NET_SERVER=/path/to/chrome_net_server zastępuje osadzony proces roboczy, na przykład lokalną kompilacją.

Jak to działa

WarstwaCo tam działa
Twój programhttp.Client i chromenet.Transport, czyste Go.
Granica procesuRPC HQQ oparte na pamięci współdzielonej. Interfejs to chrome_net.idl, kompilowany przez hqq-bindgen do klienta Go i glue'u serwera w C.
chrome_net_serverMała warstwa C++ nad Chromium: jeden wątek sieciowy, który jest właścicielem wszystkich obiektów //net, oraz wątki sesji blokujące się na żądaniu do czasu, aż wątek sieciowy odpowie.
Chromium //netURLRequestContext, URLRequest, sesje HTTP/2 i QUIC, BoringSSL.

IDL zawiera sześć istotnych operacji: utworzenie silnika z opcji key=value, rozpoczęcie żądania z jego nagłówkiem (METHOD URL oraz wiersze nagłówków) i długością ciała, zapisanie fragmentów ciała żądania, oczekiwanie na nagłówek odpowiedzi, odczyt zdekodowanego ciała oraz zniszczenie uchwytów. Uchwyt żądania deklaruje @cancel, więc porzucone wywołanie przerywa żądanie wewnątrz Chromium, zamiast pozwalać mu działać do końca.

Izolacja stosu w procesie roboczym jest równie właściwością bezpieczeństwa, co właściwością pakowania. Kod sieciowy Chromium przetwarza wrogie dane z otwartego internetu. Jeśli ulegnie awarii, Twoja usługa w Go nie ulega razem z nim: wywołanie kończy się błędem, sesja jest odrzucana, a klient uruchamia nowy proces roboczy, gdy znów będzie go potrzebował.

Budowa powtarzalna, przypinanie przez SHA-256

Plik binarny o rozmiarze 25 MB, zawierający Chromium, jest wiarygodny tylko wtedy, gdy można dokładnie określić, co weszło w jego skład. chromenet podąża za Chromium Stable tak, jak gosuda.org/sqlite3 podąża za SQLite: kod źródłowy nigdy nie jest vendorowany, jest tylko przypięty.

PrzypięcieGdzie
Wydanie Chromium i jego oficjalne archiwum źródłowe -litethird_party/chromium/VERSION
SHA-256 archiwum i każdego narzędzia przypiętego przez wydanie: clang, Rust, GN, ninja, runtime clang dla Windows, xwinthird_party/chromium/SHA256
Wersje MSVC CRT i Windows SDK pobierane przez xwinthird_party/chromium/WINSDK
Wydanie runtime piaskownicy (rungv), digest obrazu gościa, data migawki Debianascripts/engine/BUILDENV
Runtime C HQQgo.mod i go.sum

Aktualne przypięcie to Chromium 157.0.8103.38. Pobranie niezgodne z przypięciem zatrzymuje build. Wydanie stosuje dokładnie jedną łatę do Chromium: w systemie Linux chrome_net_server jest budowany bez NSS i weryfikuje certyfikaty serwerów wyłącznie przy użyciu Chrome Root Store, więc plik binarny nie wymaga niczego poza libc.

Silniki dla Linuksa i Windows są budowane wewnątrz piaskownic gvx: nieuprzywilejowanych procesów z przypiętym obrazem gościa i bez demona hosta. Silniki dla Windows są kompilowane krzyżowo na Linuksie przy użyciu clang-cl i lld-link z Chromium, przeciwko Windows SDK, które xwin pobiera od Microsoftu. Silniki dla macOS są budowane natywnie na runnerze macOS, ponieważ build Chromium dla macOS wymaga Xcode.

Ponowna budowa tego samego commita dała bajtowo identyczne pliki binarne w oddzielnych przebiegach CI. Pliki binarne wydania są bajtowo identyczne z tymi, które wcześniejszy przebieg CI zbudował dla commita wprowadzającego silnik do monorepo, na wszystkich sześciu platformach.

PlatformaRozmiar pliku binarnego
linux/amd6427.9 MB
linux/arm6427.4 MB
darwin/amd6426.2 MB
darwin/arm6423.9 MB
windows/amd6427.3 MB
windows/arm6425.8 MB

Silniki dla Linuksa zależą wyłącznie od libc, libm, libdl, libpthread i libgcc_s i wymagają glibc 2.31 lub nowszej.

Śledzenie Chromium i wydań

Zaplanowany workflow sprawdza Chromium Stable co sześć godzin. Dla nowego wydania przepisuje przypięcia, buduje i testuje wszystkie sześć silników na gałęzi chromium/<version>, poświadcza pliki binarne i commituje przypięcia razem z osadzonymi plikami binarnymi, ich skrótami i tekstami licencji do main w jednym commicie. W monorepo pliki OWNERS przyznają botowi workflow dokładnie te pliki, dzięki czemu aktualizacja trafia bez kroku ludzkiego, podczas gdy zmiany samego przepisu budowania (przypięcie Windows SDK i łata) nadal wymagają zatwierdzenia właściciela.

Każdy plik binarny wydania niesie poświadczenie pochodzenia (build provenance attestation) z workflow, który go zbudował:

1gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet

Teksty licencji tego, co zawiera każdy plik binarny (Chromium, BoringSSL, QUICHE i reszta grafu budowania), są generowane przez tools/licenses/licenses.py z samego Chromium i dołączane do wydania jako NOTICE_<os>_<arch>.txt.

Ograniczenia

  • DialWebSocket jest klientem RFC 6455 napisanym w czystym Go i nie korzysta ze stosu Chromium.
  • DialWebTransport jest implementacją działającą w tym samym procesie. Nie łączy się z serwerem.
  • Domyślne odciski JA4 i HTTP/2 są odciskami przypiętego wydania Chromium. Chromium zmienia je z wydania na wydanie, a chromenet za nimi podąża.
  • Pliki binarne macOS są podpisane ad-hoc i nie są notaryzowane.
  • Proces roboczy kosztuje pamięć. Każdy plik binarny silnika zajmuje od 24 do 28 MB na dysku, a runtime Chromium uruchamia pulę wątków oraz wątek sieciowy.
  • Sesja silnika jest zajęta do momentu zamknięcia ciała odpowiedzi. Program, który nie zamyka ciał odpowiedzi, wyczerpie pulę.

Źródło

Repozytorium to github.com/gosuda/chromenet, lustrzane odbicie //net/chromenet z monorepo gosuda. Przepis budowania znajduje się w scripts/engine, glue silnika w engine/, a interfejs w chrome_net.idl.