secubox-toolbox: install sbxmitm-policyctl (0755), the /rlevel panel +
menu.d entry, and a scoped sudoers.d/secubox-toolbox granting the portal
(User=secubox-toolbox) exactly the 5 policyctl subcommands
(set-floor|force|set-chosen|set-default|list) — no blanket ctl grant.
postinst seeds /var/lib/secubox/toolbox/peer-rlevel.json with
defaults.mode="reel" (never overwritten on upgrade, leaf-only chown) to
preserve current fleet-wide MITM behaviour instead of a surprise
fleet-wide downgrade; operators opt individual peers down afterwards.
control: + sudo (transport for the ctl delegation).
secubox-toolbox-ng: changelog bump only — rlevel.go/rlevel_test.go build
into the existing sbxmitm binary (same Go package), no rules/control
change needed. Cross-build verified: GOARCH=arm64 GOOS=linux go build
-mod=vendor produces a valid ARM aarch64 ELF; go test ./... passes in
full.
nftables.d @rlevel_off set + off-bypass rule were already shipped by the
existing execute_after_dh_auto_install block — verified, not touched.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Panneau admin.gk2/rlevel/ consommant le contrat API /rlevel de la tâche 6
(GET /rlevel/peers, POST /rlevel/peer {pubkey,floor?,forced?}, GET/POST
/rlevel/me {chosen}) : table peers avec badges off/passive/active/reel,
contrôles floor + force/unlock par ligne, carte self-service pour son
propre niveau borné par floor/forced. Navbar partagée (sidebar.js +
hybrid-dark), token sbx_token en Bearer + same-origin sur chaque appel.
Sécurité : aucune donnée API (pubkey, label) n'entre dans un handler
inline — délégation d'événements via data-action/data-pubkey + esc()
sur tout texte injecté (leçon proxypac).
menu.d/27-rlevel.json + test_rlevel_panel.py (13 cas, garde XSS incluse).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Adds GET/POST /rlevel/peers, /rlevel/peer/{pubkey}, /rlevel/me to the toolbox
FastAPI router. Admin routes list every wg-toolbox peer's rlevel status
(chosen/forced/floor/effective + best-effort live handshake) and set
floor/forced. Peer self-service resolves identity from the wg tunnel source
IP (_client_ip -> wg-peers.json), 403s on an unknown IP, and 409s if the
requested chosen mode would drop below the peer's floor or lift an admin
forced mode. Every write delegates to `sudo -n sbxmitm-policyctl`
(set-floor/force/set-chosen) -- no in-process root, mirrors the proxypac
API's _ctl pattern.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Root-scoped ctl for the per-peer wg-toolbox MITM R-level policy (task 4 of
the rlevel-per-peer plan): set-floor/force/set-chosen/set-default/list.
Shadow-write + atomic rename of peer-rlevel.json (the rename itself is the
hot-reload signal the Go workers poll for), server-side clamp of set-chosen
to the peer's floor, regeneration of the nft off-bypass drop-in from peers
whose effective mode is off (one priority below the toolbox DNAT fanout),
and an append-only JSON audit line per mutation. Invalid mode or unknown
pubkey aborts before any write.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Both sbxmitm accept paths (CONNECT/handleConnect and transparent/
handleTransparent) now go through a single decideForPeer(clientIP, host, sni)
helper: it calls the existing px.pol.Decide, then clamps the verdict to the
calling peer's R-level via px.rlevel.ModeForIP + clampVerdict when a
PeerPolicy is wired in. Factoring both call-sites onto the same helper means
they can never drift on how the clamp is applied.
Proxy gains an rlevel *PeerPolicy field, loaded in main() from
SECUBOX_PEER_RLEVEL / SECUBOX_WG_PEERS (env-overridable, falling back to
peer-rlevel.json and machash.go's wgPeersPath). A nil rlevel is a total
no-op — every Proxy built without it (existing suite, CONNECT PoC) keeps
today's exact px.pol.Decide behavior.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Joins peer-rlevel.json (pubkey -> {chosen,forced,floor}) with wg-peers.json
(pubkey -> ip) into an ip -> RLevel map. ModeForIP resolves an unknown IP to
the file's declared defaults, and any unreadable/corrupt input degrades to
Passive for every peer (fail-closed). Hot-reloads both files on mtime change,
mirroring the reload.Watcher pattern already used by policy.go.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Implémente role.detect() avec injection de probe pour tests isolés.
Master détecté si DHCP écoute UDP/67 sur l'IP LAN (tier 1).
Slave avec résolveur DNS si UDP/53 lié (tier 2), sinon tier 3.
Aucune émission de paquet DHCP (read-only passive detection).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
PAC qui dévie les .onion vers le SOCKS Tor local (SOCKS5 = remote DNS, Tor
résout le .onion), tout le reste en DIRECT (le transparent wg-toolbox inspecte
déjà). SOCKS confiné sur 192.168.1.200:9050 (SocksPolicy accept LAN+wg reject *,
jamais open-relay). Servi par nginx en application/x-ns-proxy-autoconfig,
LAN-only. Prérequis signalé : tor est inactive/failed sur le board, le plan doit
le réparer avant de livrer une pièce morte.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
L'UI réelle de picobrew_pico vit sur son sous-domaine LAN-confiné
(picobrew.gk2, avec zoom), donc une iframe pleine page dans la page admin était
redondante. La page admin devient un cockpit de supervision :
- contrôle : démarrer / arrêter / redémarrer / rafraîchir ;
- cartes statut : état, version figée, session, mémoire du conteneur ;
- bloc « accès distant » : lien Ouvrir vers l'interface complète, adresse LAN
directe, note mode zoom, redirection DNS de l'appareil ;
- journal picobrew_pico (tail du conteneur).
ctl : status --json expose mem_mb (best-effort cgroup, toujours un entier pour
ne pas casser le JSON) ; nouveau verbe restart (stop+start ; la garde de session
ne concerne que update). API : routes /restart et /logs (dégradation propre
conteneur à l'arrêt). Sudoers inchangé (tout picobrewctl déjà accordé).
Vérifié live : status mem_mb=88, /logs rend le vrai journal, panel sans iframe.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Le pipeline MITM sert un flux déchiffré avec un handler manuel (up.Do → resp →
write) : un http.Client ne peut PAS porter un « 101 Switching Protocols », donc
tout wss:// sur un hôte MITM'd traversant wg-toolbox échouait (Socket.IO de
picobrew_pico, zigbee2mqtt, toute app temps-réel). Seuls les hôtes splicés
passaient (leur io.Copy brut portait nativement le WebSocket).
Fix (comme httputil.ReverseProxy) : détecter l'upgrade (Connection: upgrade +
Upgrade: websocket), dialer upstream en forçant http/1.1 (un 101 ne peut pas
rider h2), transmettre le handshake verbatim, relayer le 101, puis piper les
octets bruts bidirectionnellement jusqu'à fermeture. Fait AVANT anonymize/DPI/
inject (un upgrade est un canal de contrôle, pas une requête inspectable ; les
en-têtes Sec-WebSocket-* doivent atteindre upstream intacts). La copie
client→upstream lit le bufio.Reader du pipeline (frames déjà bufferisées),
sinon elles seraient perdues.
dialUChrome gagne un paramètre ALPN (dialUChromeALPN) — un seul call-site — pour
que la voie WS force http/1.1 sans toucher la voie normale (h2 préféré).
Tests : détection (liste de Connection, casse, faux positifs) + relais 101 +
pipe bidirectionnel echo sur un upstream TLS réel. build + vet + suite complète
-race verts, zéro régression.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Webui admin la plus complète possible sans dev : bandeau de contrôle SecuBox
(état + start/stop/rafraîchir + plein écran) au-dessus d'un iframe de l'UI
complète de picobrew_pico, servie via son sous-domaine LAN-confiné
picobrew.gk2.secubox.in (même domaine parent → cert wildcard valide, pas de
mixed-content). L'iframe n'est chargée que conteneur en marche.
Réf. montage vhost (live gk2, patron lyrion) : sous-domaine LAN-only
(snippet exposure allow-privé/deny-all) → nginx:9080 → LXC:80, TLS terminée
par HAProxy, route dans /etc/secubox/waf/haproxy-routes.json, upgrade WebSocket
(Upgrade/Connection comme spiderfoot). CAVEAT: un wss:// via wg-toolbox échoue
tant que sbxmitm ne pipe pas l'upgrade WebSocket dans un flux MITM'd (général,
pas spécifique picobrew).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
« apt install secubox-picobrew » provisionne désormais le conteneur tout seul :
nouveau service oneshot secubox-picobrew-provision.service qui lance
`picobrewctl install` (debootstrap + clone picobrew_pico + venv + nginx/TLS),
démarré --no-block par le postinst (dpkg rend la main en ~40s, pas 5-8 min) et
gardé par ConditionPathExists=!pinned-sha (provisionne une fois, no-op ensuite).
Prouvé live sur gk2 : reset LXC → dpkg -i 2.0.1 → auto-provision en arrière-plan
→ LXC RUNNING 10.100.0.150, panel 200, /api/v1/picobrew/status JSON, appareil
:80 et TLS :443 à 200. Le webui est bien le frontend de picobrewctl via l'API.
Documente le motif comme guideline recommandée (.claude/MODULE-COMPLIANCE.md,
section « Install activates — LXC / external-service modules ») : install =
activation complète, provisionnement lourd en oneshot --no-block gardé par
marqueur, contrat ctl install (idempotent, pin-not-float, marqueur au succès
seul), et le piège de routage gk2 (webui.conf exact-match ne charge pas
secubox.d → module à socket dédié = location explicite modèle waf). Réf :
secubox-picobrew.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Réécrit le panneau selon le look management par défaut du dépôt
(hybrid-dark, Courier Prime, sidebar via /shared/sidebar.js), au lieu du
squelette minimal C3BOX. Ajoute des toasts d'action (échec start/stop plus
silencieux), vérifie response.ok, et un lien vers l'interface réelle de
l'appareil (https://<ip>/) affiché quand le conteneur tourne.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Découvert à l'installation live sur gk2 : `systemctl reload unbound` (SIGHUP)
ne recharge PAS les local-zone d'unbound — le drop-in picobrew.com n'était donc
jamais appliqué avant un restart. Préférer `unbound-control reload` quand il est
configuré, sinon try-restart, sinon reload en dernier recours (best-effort).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>