---
id: 6e0bce9e22a4bf7d9672c59e67bcf5ec
author: Lemon Mint
title: 'gosuda.org/chromenet: la pila de red de Chromium para Go, compilada de forma reproducible a partir de una versión fijada'
description: CGO_ENABLED=0 Go http.RoundTripper que envía solicitudes a través de la propia pila //net de Chromium (HTTP/1.1, HTTP/2, QUIC/HTTP/3, BoringSSL) en un worker HQQ aislado, con el código fuente, las cadenas de herramientas y seis binarios embebidos fijados mediante SHA-256 y compilados en sandboxes gvx.
language: es
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` 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](https://gosuda.org/hqq).

```bash
go 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`:

```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))
}
```

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.Client` hace ambas cosas, como ocurre con `http.Transport`.
- Las respuestas comprimidas con gzip, deflate, Brotli o zstd llegan decodificadas, y `resp.Uncompressed` es 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.NetError` que contienen el código y el nombre del error de Chromium:

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

### 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](https://gosuda.org/blog/posts/gvx-cross-platform-linux-containers-z48916d01): 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ó:

```bash
gh 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

- `DialWebSocket` es un cliente RFC 6455 en Go puro y no usa la pila de Chromium.
- `DialWebTransport` es 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 `chromenet` las 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](https://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`.
