GoSuda

gosuda.org/chromenet : la pile réseau de Chromium pour Go, compilée de manière reproductible à partir d'une version figée

By Lemon Mint views ...

gosuda.org/chromenet est un http.RoundTripper Go qui envoie les requêtes via la pile réseau propre à Chromium. Cette pile est //net issue d'une version officielle de Chromium : ses implémentations HTTP/1.1, HTTP/2 et QUIC/HTTP/3, BoringSSL, la configuration TLS de Chrome et le Chrome Root Store. Elle s'exécute dans un processus worker séparé, chrome_net_server, et le côté Go reste du Go pur (CGO_ENABLED=0), communiquant avec le worker via un RPC à mémoire partagée HQQ.

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

Pourquoi exécuter la pile réseau de Chromium

Un programme Go qui communique avec le web via net/http présente au serveur un client Go. C'est le bon choix par défaut pour le trafic entre services. C'est en revanche le mauvais lorsque le comportement du serveur dépend du type de client qu'il croit avoir en face de lui : points d'accès réservés aux navigateurs, middlewares anti-bot, CDN qui servent différentes versions de protocole selon les empreintes TLS et HTTP/2, et tests devant reproduire ce que vit réellement un navigateur.

La bibliothèque standard de Go ne peut pas reproduire le comportement réseau d'un navigateur sur le fil. crypto/tls possède son propre ordre de suites de chiffrement et son propre ensemble d'extensions, et net/http ses propres paramètres HTTP/2. Les bibliothèques qui imitent un navigateur en copiant les octets de son ClientHello doivent reproduire continuellement cette copie, version après version. chromenet emprunte une autre voie : il n'imite pas la pile réseau de Chromium, il l'exécute.

Mesurée sur le moteur darwin/arm64 face à un service public d'empreintes (tls.peet.ws), la configuration par défaut renvoie :

PropriétéValeur observée
Protocole négociéh2
JA4t13d1516h2_8daaf6152771_806a8c22fdea
Empreinte HTTP/2 (format Akamai)1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
User-AgentChrome de bureau de la version majeure du moteur, par exemple Chrome/157.0.0.0

Avec ForceHTTP3, les requêtes vers cloudflare-quic.com et www.google.com ont reçu une réponse en HTTP/3. Ces valeurs proviennent d'une seule plateforme, à un jour donné. Le moteur est la pile elle-même : elles changent donc lorsque la version de Chromium épinglée change.

Utilisation

NewTransport renvoie un http.RoundTripper. Utilisez-le partout où vous utiliseriez 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}

Lors de la première requête, le module écrit le binaire worker embarqué pour votre plateforme dans le répertoire de cache utilisateur, vérifie son empreinte SHA-256 avec gosuda.org/hqq/secureexec, le démarre avec un lien de vie parent (--parent-stdin, de sorte qu'il se termine lorsque votre processus se termine) et ouvre un pool de sessions. Aucune installation n'est nécessaire.

Certains comportements suivent http.Transport afin que http.Client fonctionne sans modification :

  • Le Transport ne suit pas les redirections et ne conserve aucun cookie. http.Client fait les deux, comme avec http.Transport.
  • Les réponses compressées en gzip, deflate, Brotli ou zstd arrivent décodées, et resp.Uncompressed vaut true.
  • Une requête détient une session du moteur jusqu'à la fermeture du corps de sa réponse. Fermez les corps.
  • L'annulation du contexte de la requête annule la requête à l'intérieur de Chromium.
  • Les échecs réseau sont des valeurs *client.NetError portant le code d'erreur Chromium et son nom :
1var ne *client.NetError
2if errors.As(err, &ne) {
3	fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
4}

Options

ChampEffet
UserAgentRemplace le User-Agent Chrome par défaut. Un en-tête de requête User-Agent reste prioritaire.
AcceptLanguageRemplace en-US,en;q=0.9.
ProxyRègles de proxy Chromium, telles que http://127.0.0.1:8080 ou socks5://127.0.0.1:1080. Vide signifie direct.
DisableHTTP2, DisableQUICDésactivent les protocoles.
ForceHTTP3Utilise QUIC pour chaque origine sans attendre une annonce Alt-Svc.
InsecureSkipVerifyAccepte n'importe quel certificat serveur.
PoolSessions du moteur à utiliser. Nil utilise client.DefaultPool(), qui autorise 64 requêtes simultanées.

Les Transports aux options égales partagent un même URLRequestContext de Chromium, de sorte que les pools de connexions, les sessions HTTP/2 et les sessions QUIC sont réutilisés d'une requête à l'autre. Définir HQQ_CHROME_NET_SERVER=/path/to/chrome_net_server remplace le worker embarqué, par exemple par une compilation locale.

Fonctionnement

CoucheCe qui s'exécute ici
Votre programmehttp.Client et chromenet.Transport, en Go pur.
Frontière de processusRPC HQQ à mémoire partagée. L'interface est chrome_net.idl, compilée par hqq-bindgen en client Go et en glue serveur C.
chrome_net_serverUne petite couche C++ au-dessus de Chromium : un thread réseau unique qui possède chaque objet //net, et des threads de session qui se bloquent sur une requête jusqu'à ce que le thread réseau réponde.
Chromium //netURLRequestContext, URLRequest, sessions HTTP/2 et QUIC, BoringSSL.

L'IDL comporte six opérations essentielles : créer un moteur à partir d'options key=value, lancer une requête avec son en-tête (METHOD URL plus les lignes d'en-tête) et la longueur du corps, écrire les fragments du corps de requête, attendre l'en-tête de réponse, lire le corps décodé et détruire les handles. Un handle de requête déclare @cancel, de sorte qu'un appel abandonné interrompt la requête à l'intérieur de Chromium au lieu de la laisser s'exécuter jusqu'au bout.

Isoler la pile dans un processus worker relève autant de la sécurité que du packaging. Le code réseau de Chromium analyse des entrées hostiles provenant d'Internet. S'il plante, votre service Go ne plante pas avec lui : l'appel échoue avec une erreur, la session est abandonnée, et le client démarre un nouveau worker la prochaine fois qu'il en a besoin.

Construction reproductible, épinglée par SHA-256

Un binaire de 25 Mo contenant Chromium n'est digne de confiance que si l'on peut dire exactement ce qui y est entré. chromenet suit Chromium Stable de la même manière que gosuda.org/sqlite3 suit SQLite : la source n'est jamais vendored, seulement épinglée.

ÉpingleEmplacement
Version de Chromium et son archive source -lite officiellethird_party/chromium/VERSION
SHA-256 de l'archive et de chaque chaîne d'outils épinglée par cette version : clang, Rust, GN, ninja, le runtime clang Windows, xwinthird_party/chromium/SHA256
Versions du CRT MSVC et du SDK Windows récupérées via xwinthird_party/chromium/WINSDK
Version du runtime de sandbox (rungv), empreinte de l'image invitée, date de snapshot Debianscripts/engine/BUILDENV
Runtime C de HQQgo.mod et go.sum

L'épingle actuelle est Chromium 157.0.8103.38. Un téléchargement ne correspondant pas à son épingle interrompt la construction. La version n'applique qu'un seul correctif à Chromium : sous Linux, chrome_net_server est compilé sans NSS et vérifie les certificats serveur uniquement avec le Chrome Root Store, de sorte que le binaire n'a besoin de rien d'autre que libc.

Les moteurs Linux et Windows sont construits dans des sandboxes gvx : des processus non privilégiés avec une image invitée épinglée et aucun démon hôte. Les moteurs Windows sont compilés croisément sous Linux avec clang-cl et lld-link de Chromium, contre le SDK Windows que xwin récupère auprès de Microsoft. Les moteurs macOS se construisent nativement sur un runner macOS, car la construction macOS de Chromium nécessite Xcode.

Reconstruire le même commit a produit des binaires identiques octet par octet lors d'exécutions CI distinctes. Les binaires de version sont identiques octet par octet à ceux qu'une exécution CI antérieure a construits pour le commit ayant introduit le moteur dans le monorepo, sur les six plateformes.

PlateformeTaille du binaire
linux/amd6427.9 Mo
linux/arm6427.4 Mo
darwin/amd6426.2 Mo
darwin/arm6423.9 Mo
windows/amd6427.3 Mo
windows/arm6425.8 Mo

Les moteurs Linux ne dépendent que de libc, libm, libdl, libpthread et libgcc_s, et nécessitent glibc 2.31 ou une version ultérieure.

Suivi de Chromium et des versions

Un workflow planifié vérifie Chromium Stable toutes les six heures. Pour une nouvelle version, il réécrit les épingles, construit et teste les six moteurs sur une branche chromium/<version>, atteste les binaires, puis valide ensemble les épingles, les binaires embarqués, leurs empreintes et les textes de licence sur main en un seul commit. Dans le monorepo, des fichiers OWNERS par fichier n'accordent au bot du workflow que ces fichiers précis, de sorte que la mise à jour est intégrée sans intervention humaine, tandis que les modifications de la recette de construction elle-même (l'épingle du SDK Windows et le correctif) nécessitent toujours un responsable.

Chaque binaire de version porte une attestation de provenance de construction émise par le workflow qui l'a construit :

1gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet

Les textes de licence de ce que contient chaque binaire (Chromium, BoringSSL, QUICHE et le reste du graphe de construction) sont générés par tools/licenses/licenses.py de Chromium lui-même et livrés avec la version sous la forme NOTICE_<os>_<arch>.txt.

Limites

  • DialWebSocket est un client RFC 6455 en Go pur et n'utilise pas la pile Chromium.
  • DialWebTransport est une implémentation in-process. Elle n'atteint pas de serveur.
  • Les empreintes JA4 et HTTP/2 par défaut sont celles de la version de Chromium épinglée. Chromium les modifie d'une version à l'autre, et chromenet les suit.
  • Les binaires macOS sont signés ad hoc et non notariés.
  • Un processus worker coûte de la mémoire. Chaque binaire de moteur pèse 24 à 28 Mo sur disque, et le runtime de Chromium démarre un pool de threads et un thread réseau.
  • Une session de moteur est détenue jusqu'à la fermeture du corps de la réponse. Un programme qui laisse fuir les corps de réponse épuisera le pool.

Source

Le dépôt est github.com/gosuda/chromenet, miroir de //net/chromenet du monorepo gosuda. La recette de construction se trouve dans scripts/engine, la glue du moteur dans engine/, et l'interface dans chrome_net.idl.