gosuda.org/chromenet: la pila de red de Chromium para Go, compilada de forma reproducible a partir de una versión fijada

gosuda.org/chromenet es un http.RoundTripper de Go que envía solicitudes a través de la pila de red propia de Chromium. La pila es //net de una versión oficial de Chromium: sus implementaciones de HTTP/1.1, HTTP/2 y QUIC/HTTP/3, BoringSSL, la configuración TLS de Chrome y el Chrome Root Store. Se ejecuta en un proceso worker separado, chrome_net_server, y la parte en Go se mantiene como Go puro (CGO_ENABLED=0), comunicándose con el worker mediante RPC de memoria compartida HQQ.
1go get gosuda.org/chromenet@v0.1.10157081033800
Por qué ejecutar la pila de red de Chromium
Un programa en Go que se comunica con la web mediante net/http presenta al servidor un cliente Go. Ese es el valor predeterminado correcto para el tráfico entre servicios. Es el incorrecto cuando el comportamiento del servidor depende del tipo de cliente que cree tener delante: endpoints que exigen un navegador, middleware antibot, CDNs que sirven distintas versiones de protocolo según las huellas TLS y HTTP/2, y pruebas que deben reproducir lo que experimenta un navegador real.
La biblioteca estándar de Go no puede reproducir el comportamiento de red de un navegador. crypto/tls tiene su propio orden de suites de cifrado y su propio conjunto de extensiones, y net/http tiene sus propios ajustes de HTTP/2. Las bibliotecas que imitan a un navegador copiando los bytes de su ClientHello siguen copiándolos, versión tras versión. chromenet sigue el camino contrario: no imita la pila de red de Chromium, sino que la ejecuta.
Medido en el motor darwin/arm64 contra un servicio público de huellas (tls.peet.ws), la configuración predeterminada informa:
| Propiedad | Valor observado |
|---|---|
| Protocolo negociado | h2 |
| JA4 | t13d1516h2_8daaf6152771_806a8c22fdea |
| Huella HTTP/2 (formato Akamai) | 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p |
| User-Agent | Chrome de escritorio de la versión mayor del motor, p. ej. Chrome/157.0.0.0 |
Con ForceHTTP3, las solicitudes a cloudflare-quic.com y www.google.com recibieron respuesta mediante HTTP/3. Estas cifras proceden de una plataforma en un día concreto. El motor es la pila, por lo que cambian cuando cambia la versión de Chromium fijada.
Cómo usarlo
NewTransport devuelve un http.RoundTripper. Úselo en cualquier lugar donde usaría 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}
En la primera solicitud, el módulo escribe el binario del worker incrustado para su plataforma en el directorio de caché del usuario, verifica su digest SHA-256 con gosuda.org/hqq/secureexec, lo inicia con un vínculo de vida con el proceso padre (--parent-stdin, de modo que termina cuando termina su proceso) y abre un pool de sesiones. No es necesario instalar nada.
Algunos comportamientos siguen a http.Transport para que http.Client funcione sin cambios:
- El Transport no sigue redirecciones ni mantiene cookies.
http.Clienthace ambas cosas, como ocurre conhttp.Transport. - Las respuestas comprimidas con gzip, deflate, Brotli o zstd llegan decodificadas, y
resp.Uncompressedes true. - Una solicitud mantiene una sesión del motor hasta que se cierra el cuerpo de su respuesta. Cierre los cuerpos.
- Cancelar el contexto de la solicitud cancela la solicitud dentro de Chromium.
- Los fallos de red son valores
*client.NetErrorque contienen el código y el nombre del error de Chromium:
1var ne *client.NetError
2if errors.As(err, &ne) {
3 fmt.Println(ne.Code, ne.Name) // -105 ERR_NAME_NOT_RESOLVED
4}
Opciones
| Campo | Efecto |
|---|---|
UserAgent | Reemplaza el User-Agent predeterminado de Chrome. Un encabezado User-Agent de la solicitud sigue prevaleciendo. |
AcceptLanguage | Reemplaza en-US,en;q=0.9. |
Proxy | Reglas de proxy de Chromium, como http://127.0.0.1:8080 o socks5://127.0.0.1:1080. Vacío significa conexión directa. |
DisableHTTP2, DisableQUIC | Desactivan los protocolos. |
ForceHTTP3 | Usa QUIC para todos los orígenes sin esperar un anuncio Alt-Svc. |
InsecureSkipVerify | Acepta cualquier certificado de servidor. |
Pool | Sesiones del motor a utilizar. Nil usa client.DefaultPool(), que permite 64 solicitudes concurrentes. |
Los transports con opciones iguales comparten un único URLRequestContext de Chromium, de modo que los pools de conexiones, las sesiones HTTP/2 y las sesiones QUIC se reutilizan entre solicitudes. Definir HQQ_CHROME_NET_SERVER=/path/to/chrome_net_server reemplaza el worker incrustado, por ejemplo, por una compilación local.
Cómo funciona
| Capa | Qué se ejecuta allí |
|---|---|
| Su programa | http.Client y chromenet.Transport, Go puro. |
| Frontera de proceso | RPC de memoria compartida de HQQ. La interfaz es chrome_net.idl, compilada por hqq-bindgen en el cliente Go y en la capa de integración del servidor C. |
chrome_net_server | Una pequeña capa C++ sobre Chromium: un hilo de red que posee todos los objetos de //net, y hilos de sesión que se bloquean esperando una solicitud hasta que el hilo de red responde. |
Chromium //net | URLRequestContext, URLRequest, sesiones HTTP/2 y QUIC, BoringSSL. |
La IDL tiene seis operaciones relevantes: crear un motor a partir de opciones key=value, iniciar una solicitud con su cabecera (METHOD URL más las líneas de encabezado) y la longitud del cuerpo, escribir fragmentos del cuerpo de la solicitud, esperar la cabecera de la respuesta, leer el cuerpo decodificado y destruir los manejadores. Un manejador de solicitud declara @cancel, de modo que una llamada abandonada interrumpe la solicitud dentro de Chromium en lugar de dejarla ejecutarse hasta el final.
Aislar la pila en un proceso worker es una propiedad de seguridad además de una cuestión de empaquetado. El código de red de Chromium analiza entradas hostiles procedentes de internet abierta. Si se bloquea, su servicio en Go no se bloquea con él: la llamada falla con un error, la sesión se descarta y el cliente inicia un nuevo worker la próxima vez que lo necesite.
Compilación reproducible, fijada por SHA-256
Un binario de 25 MB que contiene Chromium solo es confiable si se puede saber exactamente qué entró en él. chromenet sigue a Chromium Stable de la misma forma que gosuda.org/sqlite3 sigue a SQLite: el código fuente nunca se incluye en el repositorio (vendored), solo se fija.
| Fijación | Ubicación |
|---|---|
Versión de Chromium y su archivo oficial de código fuente -lite | third_party/chromium/VERSION |
| SHA-256 del archivo y de cada herramienta de compilación que esa versión fija: clang, Rust, GN, ninja, el runtime de clang para Windows, xwin | third_party/chromium/SHA256 |
| Versiones del CRT de MSVC y del Windows SDK obtenidas mediante xwin | third_party/chromium/WINSDK |
Versión del runtime de sandbox (rungv), digest de la imagen invitada y fecha de la instantánea de Debian | scripts/engine/BUILDENV |
| El runtime C de HQQ | go.mod y go.sum |
La fijación actual es Chromium 157.0.8103.38. Una descarga que no coincida con su fijación detiene la compilación. La versión aplica exactamente un parche a Chromium: en Linux, chrome_net_server se compila sin NSS y verifica los certificados de servidor solo con el Chrome Root Store, de modo que el binario no necesita nada más que libc.
Los motores de Linux y Windows se compilan dentro de sandboxes de gvx: procesos sin privilegios con una imagen invitada fijada y sin demonio en el host. Los motores de Windows se compilan de forma cruzada en Linux con clang-cl y lld-link de Chromium, contra el Windows SDK que xwin obtiene de Microsoft. Los motores de macOS se compilan de forma nativa en un runner de macOS, porque la compilación de Chromium para macOS requiere Xcode.
Recompilar el mismo commit produjo binarios idénticos byte a byte en ejecuciones de CI separadas. Los binarios de publicación son idénticos byte a byte a los que una ejecución anterior de CI compiló para el commit que incorporó el motor en el monorepo, en las seis plataformas.
| Plataforma | Tamaño del binario |
|---|---|
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 |
Los motores de Linux dependen solo de libc, libm, libdl, libpthread y libgcc_s, y requieren glibc 2.31 o posterior.
Seguimiento de Chromium y publicaciones
Un flujo de trabajo programado revisa Chromium Stable cada seis horas. Ante una nueva versión, reescribe las fijaciones, compila y prueba los seis motores en una rama chromium/<version>, certifica los binarios y confirma en main, en un solo commit, las fijaciones junto con los binarios incrustados, sus digests y los textos de licencia. En el monorepo, los OWNERS por archivo otorgan al bot del flujo de trabajo exactamente esos archivos, de modo que la actualización se incorpora sin un paso humano; sin embargo, los cambios en la receta de compilación en sí (la fijación del Windows SDK y el parche) siguen requiriendo un propietario.
Cada binario de publicación lleva una atestación de procedencia de compilación emitida por el flujo de trabajo que lo compiló:
1gh attestation verify chrome_net_server_linux_amd64 --repo gosuda/chromenet
Los textos de licencia de lo que contiene cada binario (Chromium, BoringSSL, QUICHE y el resto del grafo de compilación) son generados por tools/licenses/licenses.py de Chromium y se distribuyen con la publicación como NOTICE_<os>_<arch>.txt.
Limitaciones
DialWebSocketes un cliente RFC 6455 en Go puro y no usa la pila de Chromium.DialWebTransportes una implementación en proceso. No llega a ningún servidor.- Las huellas JA4 y HTTP/2 predeterminadas son las de la versión de Chromium fijada. Chromium las cambia de una versión a otra, y
chromenetlas sigue. - Los binarios de macOS están firmados ad hoc y no están notarizados.
- Un proceso worker consume memoria. Cada binario del motor ocupa de 24 a 28 MB en disco, y el runtime de Chromium inicia un pool de hilos y un hilo de red.
- Una sesión del motor se mantiene hasta que se cierra el cuerpo de la respuesta. Un programa que filtre cuerpos de respuesta agotará el pool.
Fuente
El repositorio es github.com/gosuda/chromenet, reflejado desde //net/chromenet en el monorepo de gosuda. La receta de compilación está en scripts/engine, la capa de integración del motor en engine/ y la interfaz en chrome_net.idl.