Plugins nsjail
Le niveau nsjail offre la plus grande flexibilité en exécutant des plugins en tant que processus enfants isolés. Tout exécutable (scripts Python, gemmes Ruby, binaires Go compilés, scripts shell) peut être un plugin bext tant qu'il utilise le protocole IPC JSON-over-stdio. Il s'agit d'une fonctionnalité Pro.
Couches d'isolation#
bext applique l'isolation progressivement en fonction de ce que le système hôte prend en charge :
| Couche | Exigence | Ce qu'il fait |
|---|---|---|
| Isolation des processus | Toujours | Processus séparé, canalisation stdio, délai d'attente de l'horloge murale |
| Espace de noms PID | Linux + racine | Le plugin ne peut pas voir ou signaler d'autres processus |
| Espace de noms NET | Linux + racine | Le plugin obtient une pile réseau isolée (pas d'accès au réseau hôte) |
| Espace de noms de montage | Linux + racine | Le plugin obtient une vue minimale du système de fichiers |
| Limites du groupe C | Linux + racine + groupes de contrôle v2 | Limites strictes du processeur et de la mémoire |
| seccomp-bpf | Linux + racine + fonctionnalité eBPF | Liste autorisée des appels système |
Sans root sous Linux (ou sur macOS), les plugins s'exécutent toujours en tant que processus distincts avec un stdio canalisé et des délais d'attente d'horloge murale configurables. C’est suffisant pour de nombreux cas d’utilisation.
Protocole IPC#
bext communique avec les processus du plugin via des lignes JSON sur stdin/stdout. Chaque événement du cycle de vie est une seule ligne JSON envoyée au stdin de l'enfant ; l'enfant répond avec une seule ligne JSON sur la sortie standard. stderr est capturé en tant que sortie de journal.
Hôte vers plugin (stdin)#
{"method": "onServerStart", "params": {"config": {"threshold": 100}}}
{"method": "onRequestComplete", "params": {"path": "/api/users", "status": 200, "render_time_us": 1250}}
{"method": "onCacheWrite", "params": {"key": "page:/about", "tags": ["page", "about"]}}
{"method": "cleanup", "params": null}
Plugin vers l'hôte (stdout)#
{"ok": true}
{"ok": true}
{"error": "connection refused"}
Une ligne vide ou une réponse manquante est traitée comme {"ok": true} – les hooks sont facultatifs.
Configuration#
[plugins.fraud-detector]
sandbox = "nsjail"
path = "./plugins/fraud-detector/main.py"
priority = 650
[plugins.fraud-detector.sandbox]
max_memory_mb = 128
max_time_secs = 30
storage_quota_kb = 10240
interpreter = "python3" # Optional: auto-detected from shebang or extension
[plugins.fraud-detector.config]
model_path = "./models/fraud-v3.bin"
threshold = 0.85
Détection d'interprète#
Si interpreter n'est pas défini, bext le détecte automatiquement :
- Shebang - lit la première ligne.
#!/usr/bin/env python3est résolu enpython3.#!/usr/bin/rubyest résolu en/usr/bin/ruby. - Extension de fichier -
.pycorrespond àpython3,.rbàruby,.jsànode,.shàsh. - Valeur par défaut --
/bin/sh.
Pour les binaires compilés (sans extension, en-tête ELF), le fichier est exécuté directement.
Limites des ressources#
Mémoire#
Avec cgroups v2, définissez une limite de mémoire stricte :
[plugins.my-plugin.sandbox]
max_memory_mb = 256
Le MOO du noyau tue le processus du plugin s'il dépasse cette limite. bext détecte la résiliation et l'enregistre en tant que crash du plugin. Le plugin est redémarré lors du prochain événement du cycle de vie.
Processeur#
Les délais d'attente de l'horloge murale empêchent les plugins incontrôlables :
[plugins.my-plugin.sandbox]
max_time_secs = 10
Si le plugin ne répond pas dans le délai imparti, bext envoie SIGKILL et renvoie une erreur pour cet appel de hook. La longueur de ligne maximale des messages IPC est de 10 Mo pour éviter l'épuisement de la mémoire dû à une sortie malveillante.
Stockage#
Chaque plugin possède un répertoire de stockage limité à data/plugins/<plugin-name>/. Le quota de stockage est appliqué par bext (et non par le système d'exploitation) :
[plugins.my-plugin.sandbox]
storage_quota_kb = 10240 # 10 MB
Exemple complet : plugin Python Analytics#
#!/usr/bin/env python3
"""bext lifecycle plugin that sends analytics to a data warehouse."""
import json
import sys
import urllib.request
config = {}
buffer = []
BATCH_SIZE = 50
def handle_message(msg):
method = msg.get("method")
params = msg.get("params", {})
if method == "onServerStart":
global config
config = params.get("config", {})
return {"ok": True}
elif method == "onRequestComplete":
buffer.append({
"path": params.get("path"),
"status": params.get("status"),
"render_time_us": params.get("render_time_us"),
"cache_status": params.get("cache_status"),
})
if len(buffer) >= BATCH_SIZE:
flush()
return {"ok": True}
elif method == "cleanup":
flush()
return {"ok": True}
return {"ok": True}
def flush():
global buffer
if not buffer or "endpoint" not in config:
return
payload = json.dumps({"events": buffer}).encode()
req = urllib.request.Request(
config["endpoint"],
data=payload,
headers={"Content-Type": "application/json"},
)
try:
urllib.request.urlopen(req, timeout=5)
except Exception as e:
print(f"flush error: {e}", file=sys.stderr)
buffer = []
# Main IPC loop
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
result = handle_message(msg)
except Exception as e:
result = {"error": str(e)}
print(json.dumps(result), flush=True)
Configurez-le :
[plugins.analytics-warehouse]
sandbox = "nsjail"
path = "./plugins/analytics/main.py"
[plugins.analytics-warehouse.sandbox]
max_memory_mb = 64
max_time_secs = 15
[plugins.analytics-warehouse.config]
endpoint = "https://warehouse.example.com/ingest"
Cas d'utilisation#
Le niveau nsjail est le bon choix lorsque :
- Plugins multi-locataires : les clients téléchargent leurs propres scripts. L'isolation des processus les empêche d'accéder aux données des autres ou au système hôte.
- Outils existants -- vous souhaitez intégrer un modèle Python ML, un Ruby DSL ou un pipeline de script shell sans réécrire en JS ou Rust.
- Code non fiable : avec une isolation complète de l'espace de noms, même un plugin malveillant ne peut pas échapper à son bac à sable.
- Dépendances lourdes - plugins nécessitant des bibliothèques système (NumPy, OpenCV, FFmpeg) qui ne peuvent pas être compilées dans WASM.
Limites#
- Surcharge de démarrage -- ~ 10 ms par appel de cycle de vie en raison de l'apparition du processus (contre < 1 ms pour QuickJS/WASM). Le processus du plugin reste actif entre les appels pour amortir ce coût.
- Aucune capacité middleware -- les plugins nsjail prennent en charge uniquement les hooks de cycle de vie, et non les middlewares par requête. Pour l'interception du chemin de requête, utilisez QuickJS ou WASM.
- Isolation de l'espace de noms Linux uniquement -- sur macOS et Linux non root, vous bénéficiez d'une isolation des processus et de délais d'attente, mais pas du renforcement de l'espace de noms/cgroup/seccomp.
Les plugins nsjail prennent en charge les hooks de cycle de vie uniquement — ils ne peuvent pas intercepter ou modifier les requêtes HTTP individuelles. Si vous avez besoin d'un middleware par requête (blocage d'une requête, réécriture d'une réponse), utilisez plutôt Plugins QuickJS ou Plugins WASM.
Le processus du plugin reste actif entre les appels de hook, donc le coût d'apparition d'environ 10 ms est payé une fois au démarrage du serveur, et non par requête. Gardez le processus du plugin en cours d'exécution (lu depuis stdin en boucle) plutôt que de quitter après chaque réponse pour en bénéficier.
En rapport#
- Présentation du système de plugins — niveaux de sandbox, types de fonctionnalités et quand les utiliser
- Plugins QuickJS — sandbox JS léger en cours de processus pour middleware par requête
- Plugins WASM — isolation forte avec des performances quasi natives, aucun coût de génération de processus
- Plugins Python — Python via Pyodide dans le runtime WASM (pas de shell, mais pas de surcharge d'apparition)
- Plugins Lua – petits scripts en cours pour les décisions politiques
- Édition — partagez votre plugin nsjail sur le registre