---
id: 6e0bce9e22a4bf7d9672c59e67bcf5ec
author: Lemon Mint
title: 'gosuda.org/chromenet : la pile réseau de Chromium pour Go, compilée de manière reproductible à partir d''une version figée'
description: CGO_ENABLED=0 Go http.RoundTripper qui envoie les requêtes via la pile //net propre à Chromium (HTTP/1.1, HTTP/2, QUIC/HTTP/3, BoringSSL) dans un worker HQQ isolé, avec les sources, les toolchains et six binaires embarqués épinglés par SHA-256 et construits dans des sandboxes gvx.
language: fr
date: 2026-11-09T00:00:00Z
path: /chromenet
go_package: gosuda.org/chromenet
go_repourl: https://github.com/gosuda/chromenet.git
category: network
---

`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](https://gosuda.org/hqq).

```bash
go 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`                                                                  |
| JA4                                | `t13d1516h2_8daaf6152771_806a8c22fdea`                                |
| Empreinte HTTP/2 (format Akamai)   | `1:65536;2:0;4:6291456;6:262144\|15663105\|0\|m,a,s,p`                |
| User-Agent                         | Chrome 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` :

```go
package main

import (
	"fmt"
	"io"
	"net/http"

	"gosuda.org/chromenet"
)

func main() {
	client := &http.Client{Transport: chromenet.NewTransport(chromenet.Options{})}

	resp, err := client.Get("https://example.com/")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Proto, resp.StatusCode, len(body))
}
```

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 :

```go
var ne *client.NetError
if errors.As(err, &ne) {
	fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
}
```

### Options

| Champ                         | Effet                                                                                                  |
| :---------------------------- | :----------------------------------------------------------------------------------------------------- |
| `UserAgent`                   | Remplace le User-Agent Chrome par défaut. Un en-tête de requête `User-Agent` reste prioritaire.        |
| `AcceptLanguage`              | Remplace `en-US,en;q=0.9`.                                                                             |
| `Proxy`                       | Règles de proxy Chromium, telles que `http://127.0.0.1:8080` ou `socks5://127.0.0.1:1080`. Vide signifie direct. |
| `DisableHTTP2`, `DisableQUIC` | Désactivent les protocoles.                                                                            |
| `ForceHTTP3`                  | Utilise QUIC pour chaque origine sans attendre une annonce `Alt-Svc`.                                  |
| `InsecureSkipVerify`          | Accepte n'importe quel certificat serveur.                                                             |
| `Pool`                        | Sessions 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

| Couche              | Ce qui s'exécute ici                                                                                                                                                   |
| :------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Votre programme     | `http.Client` et `chromenet.Transport`, en Go pur.                                                                                                                     |
| Frontière de processus | RPC 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_server` | Une 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 `//net`    | `URLRequestContext`, `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.

| Épingle                                                                                                                  | Emplacement                    |
| :----------------------------------------------------------------------------------------------------------------------- | :----------------------------- |
| Version de Chromium et son archive source `-lite` officielle                                                             | `third_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, xwin | `third_party/chromium/SHA256`  |
| Versions du CRT MSVC et du SDK Windows récupérées via xwin                                                               | `third_party/chromium/WINSDK`  |
| Version du runtime de sandbox (`rungv`), empreinte de l'image invitée, date de snapshot Debian                           | `scripts/engine/BUILDENV`      |
| Runtime C de HQQ                                                                                                         | `go.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](https://gosuda.org/blog/posts/gvx-cross-platform-linux-containers-z48916d01) : 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.

| Plateforme      | Taille du binaire |
| :-------------- | ----------------: |
| `linux/amd64`   |           27.9 Mo |
| `linux/arm64`   |           27.4 Mo |
| `darwin/amd64`  |           26.2 Mo |
| `darwin/arm64`  |           23.9 Mo |
| `windows/amd64` |           27.3 Mo |
| `windows/arm64` |           25.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 :

```bash
gh 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](https://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`.
