🛡️ sandbox-labs GitHub ↗

10 · Construcción de paquetes de terceros#

En una frase, para cualquiera: compilar un programa descarga y ejecuta código de cientos de personas que nunca verás. Este caso deja la red abierta mientras se descarga y la cierra mientras se compila.

Estado real: 🟡 building — hay código y 6 comprobaciones automáticas, sin levantarse bajo bwrap en CI · Carpeta: cases/10-package-build/ · Puerto: 8810


Por qué se realiza este caso#

Construir software moderno tiene dos fases que se suelen confundir en una:

  1. Resolver dependencias — descargar lo que hace falta. Necesita red.
  2. Compilar — ejecutar los scripts de construcción de todo eso. **No

necesita red**, y sin embargo casi siempre la tiene.

Esa segunda fase ejecuta código arbitrario de cada dependencia, con tus permisos. Si además tiene red abierta, tiene todo lo necesario para sacar lo que encuentre.

MomentoLo que corre¿Necesita red?¿La tiene normalmente?
ResoluciónEl gestor de paquetes
postinstallScripts de cada dependenciaNo
CompilaciónCompiladores y generadoresNo
EmpaquetadoHerramientas de empaquetadoNo

La idea que enseña, y que ningún otro caso enseña#

Cerrar la red a mitad del proceso. No es un control binario aplicado al principio: es un control que cambia durante la ejecución, cuando se ha obtenido lo necesario y ya no hay motivo legítimo para seguir conectado.

Es un control barato —el momento del cambio está perfectamente definido— y casi nadie lo aplica.

Casos de uso reales#

Cómo funcionará#

flowchart LR
  M["📜 Manifiesto<br/>de dependencias"] --> F1
  subgraph F1["🌐 Fase 1 · red por lista de permitidos"]
    R["📥 Resolver y descargar"]
    V["🔐 Verificar checksums"]
    R --> V
  end
  F1 --> F2
  subgraph F2["🔒 Fase 2 · red NONE"]
    B["🔨 postinstall + compilación"]
  end
  F2 --> O["📦 Artefacto"]
  F2 --> S["📋 SBOM"]
flowchart TB
  A["Descarga completada"] --> B{"¿Todos los checksums<br/>coinciden con el lockfile?"}
  B -- no --> C["🚫 No se construye"]
  B -- sí --> D["🔌 Cerrar la red"]
  D --> E["🔨 Construir sin red"]
  E --> F{"¿Algo intentó<br/>conectarse?"}
  F -- sí --> G["📣 Se anota: una dependencia<br/>quería salir durante la compilación"]
  F -- no --> H["✅ Construcción limpia"]

Ese ¿Algo intentó conectarse? es información valiosa por sí sola: una dependencia que intenta salir a internet mientras compila es una señal, la construcción funcione o no.

Esquemas#

Entrada#

{
  "manifest": "package.json",
  "lockfile": "pnpm-lock.yaml",
  "resolveAllowlist": ["registry.npmjs.org:443"],
  "buildNetwork": "none"
}

Salida#

{
  "outcome": "built",
  "sbom": [{ "name": "left-pad", "version": "1.3.0", "sha256": "…" }],
  "buildNetworkAttempts": [
    { "from": "postinstall de paquete-x", "host": "203.0.113.9:443", "outcome": "sin red: fallo de resolución" }
  ],
  "cacheHit": true
}

Software necesario#

ComponentePara qué
Rust 1.75+El supervisor de las dos fases
bubblewrap 0.6+Jaulas distintas para cada fase
El proxy de salida con lista de permitidosYa construido: la red de la fase 1
pnpm 9+ / cargo / el gestor que apliqueLa resolución real
Linux o WSL2Namespaces sin privilegios
Este proyecto usa pnpm, nunca npm, y conserva sus ficheros de bloqueo. Un Cargo.lock o un pnpm-lock.yaml versionado es lo que hace posible verificar checksums antes de cerrar la red.

Instalación#

sudo apt install bubblewrap
corepack enable pnpm
cargo build --release

Procesos que se crearán#

sandboxctl build <proyecto>
  │
  ├─ FASE 1
  │   ├─ proxy de salida         ← lista de permitidos: solo el registro
  │   └─ bwrap → gestor de paquetes
  │
  └─ FASE 2
      └─ bwrap → scripts de construcción   ← SIN red, caché montada de solo lectura

Son dos jaulas distintas, no una que cambia. Es más simple de razonar y no deja ninguna ventana en la que la red siga abierta por descuido.

Tiempo de carga estimado#

OperaciónCoste esperado
Fase 1 con caché calientesegundos
Fase 1 sin cachélo que tarde la descarga
Cambio de fase5–15 ms: es arrancar la segunda jaula
Fase 2lo que tarde el compilador

Qué hace falta para construirlo#

  1. Orquestación de las dos fases con jaulas separadas.
  2. Verificación de checksums contra el fichero de bloqueo antes de cerrar la red.
  3. Caché compartida montada de solo lectura en la fase 2.
  4. Generación de SBOM a partir de lo resuelto.
  5. Registro de intentos de red durante la compilación.

Si algo falla#

El caso ya tiene código: el núcleo en core.py y el servicio en app.py. Lo que sigue son sus fallos, la causa y la salida:

SituaciónCausaCómo se resuelve
La compilación falla al cerrar la redUna dependencia descarga algo durante el postinstallEs el hallazgo del caso. Alternativas: 1. Preparar esa descarga en la fase 1 y montarla. 2. Sustituir la dependencia. 3. Documentar la excepción y añadir el destino a la lista de la fase 1 — nunca abrir la red en la fase 2
Un checksum no coincide con el fichero de bloqueoLo descargado no es lo esperadoNo se construye. Puede ser un registro con caché sucia, o el paquete cambió bajo la misma versión. Regenerar el fichero de bloqueo a conciencia, no borrarlo
La caché no se usaEstá montada de solo lectura en la fase 2Es deliberado: una caché escribible en la fase de compilación deja que un paquete prepare la construcción del siguiente. El llenado de caché ocurre en la fase 1
pnpm: command not foundNo está habilitadocorepack enable pnpm. No uses npm: mezclar gestores produce árboles distintos entre tu equipo y CI
El SBOM sale incompletoSe generó desde el manifiesto y no desde lo resueltoSe genera desde el árbol resuelto de la fase 1, que es el único que conoce las transitivas

Los fallos que afectan a cualquier caso —no se puede crear el sandbox, no hay cgroups, un puerto ocupado, procesos huérfanos, la compilación en Windows— están resueltos uno a uno en Cuando algo falla.

Cómo se comprueba#

node scripts/verify-cases.mjs

Llama al núcleo del caso con situaciones concretas y comprueba qué hizo con ellas, no cómo está escrito. Son 6 comprobaciones, y corren en cada commit.

cargo run -p sandboxctl -- service up package-build

Levanta el caso como producto en 127.0.0.1:8810. POST /api/run acepta el cuerpo que describen los esquemas de arriba.

Sigue en building, no en functional. El núcleo se comprueba, pero el servicio no se levanta bajo bwrap dentro de CI y el caso no emite evidencia firmada. La regla completa está en el ROADMAP.

Ver también: Catálogo completo · Caso 15 · cadena de suministro · Caso 09 · CI