- new wiki page Android-ToolBox.md (install via /wg/toolbox.apk, manual +
root onboarding flows, CI build, endpoints) + sidebar link
- README: document the root-mode silent path + revised constraints
- WIP/TODO/HISTORY: Android client section (#531/#536/#538)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an optional one-tap, zero-interaction onboarding path for rooted
devices. When root is detected the app can:
- install the village3b CA into the SYSTEM trust store (bind-mount over
/system/etc/security/cacerts + conscrypt APEX, SELinux ctx restored),
so every app trusts the cabine CA — not just user-CA opt-in apps;
- bring the WireGuard tunnel up natively via the kernel module
(ip link add … type wireguard + wg set), no WireGuard app needed;
- verify R3 reachability automatically.
Falls back to the existing manual handoff (KeyChain CA prompt + WG app)
when the kernel lacks WireGuard. All root actions are gated behind an
explicit '⚡ Installation automatique (root)' tap — nothing runs as root
without the operator choosing it on their own device.
New: RootShell (su wrapper), RootOnboard (silent sequence + subject_hash_old
in pure Kotlin). MainActivity gains a RootAuto step with a streaming log.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The export/json/recent/ endpoint renamed the IOC field ioc → ioc_value;
the parser still read e.get('ioc') and skipped every entry, ingesting 0
for weeks and starving Phase 13.B DNS-guard of domain IOCs. Now reads
ioc_value or ioc. Verified live on gk2: threatfox 0 → 5000 ingested
(2935 domains + 1613 ips + 32 sha256). changelog 2.6.12.
After the wg-admin tunnel is confirmed working, 'harden' closes the
public SSH brute-force surface:
- sshd drop-in: PasswordAuthentication no + PermitRootLogin
prohibit-password (refuses if no root authorized_keys — no lockout).
- nft SSH-guard: drop tcp/22 from sources outside the LAN (192.168.1.0/24)
+ 10.x tunnels, inserted BEFORE the blanket 'iif eth1 accept' so the
router's port-forwarded public SSH (real public src IP, DNAT no-SNAT)
is dropped while LAN + wg-admin stay reachable. Persisted in
/etc/nftables.conf + applied live without flushing other tables.
Applied + verified on gk2: tunnel SSH (10.98.0.1) key-only works; public
admin.gk2.secubox.in:22 now times out. Tables (blacklist/wg) intact.
The while-read in write_server_conf reused the global pip/ppub names, so
after it ran the caller's $pip (the new peer IP for the client config)
was wiped to empty — the emitted client config showed 'Address = /32'.
The provisioned server state was always correct; only the printed config
was wrong. Loop now uses local _pn/_pip/_pub.
Idempotent provisioner for a dedicated out-of-band admin tunnel,
distinct from wg-toolbox:
- wg-admin UDP 51821, server 10.98.0.1/24, peers 10.98.0.2+/32.
- generates server+peer keys once (0600, never in repo), writes
/etc/wireguard/wg-admin.conf from a peers state file.
- installs /etc/nftables.d/secubox-admin-wg.nft (allow udp/51821,
persistent) — ADDITIVE, does not touch sshd or existing rules.
- 'add <name>' mints a client .conf + QR (split-tunnel, endpoint
admin.gk2.secubox.in:51821).
sshd hardening (key-only + restrict 22) is a deliberate separate step.
Closes the DoH / hardcoded-IP bypass gap on top of 13.A.
- secubox-blacklist-sync: Source 3 resolves threat_intel domain IOCs
(ioc_type='domain') to A/AAAA via getent (2s per-lookup timeout,
2000-domain cap) and pushes the IPs into blacklist_v4/v6 — a device
that resolves a known-bad domain out-of-band (DoH / hardcoded IP) is
still dropped at forward. Writes /run/secubox/blacklist-sync.json.
- secubox-blacklist.nft: doh_detect_v4/v6 sets + count-only doh_watch
chain (priority -11) countering device egress to known DoH/DoT
providers (Cloudflare/Google/Quad9/AdGuard/OpenDNS/CleanBrowsing/
Control-D/NextDNS) on tcp 443/853. Made the drop-in IDEMPOTENT via
the create-or-replace idiom (table{} ; delete table ; table{...}) so
nft -f reloads don't append duplicate rules.
- secubox-blacklist-sync: populates DoH sets each cycle;
SECUBOX_DOH_BLOCK=1 also enforce-drops them (forces devices back
through Vortex DNS). Default off — intrusive, gated per #519 Q1.
- api.py /admin/blacklist: doh_detect counts, doh_hits, resolved_domains,
doh_block flag.
- changelog 2.6.9.
Live on gk2: idempotency verified (enforce=4 / doh_watch=2 rules after
double reload), endpoint shows v4_count 20 + doh_detect 15 v4 / 6 v6,
doh_block false. resolved_domains 0 (threat_intel has no domain IOCs
yet — threatfox feed currently returns 0; path is correct).
First track of the protection enforcement plane (#519). Pure netfilter,
additive drops, DEFAULT-DROP doctrine preserved.
- nftables.d/secubox-blacklist.nft: table inet secubox_blacklist with
blacklist_v4/v6 sets (interval,timeout) + a single forward-hook
chain (priority -10, policy accept) counter-dropping any routed
packet whose saddr OR daddr is blacklisted. One rule set covers
every device egress path (captive/WG/br-lxc/LAN), no per-iface logic.
- sbin/secubox-blacklist-sync: unions CrowdSec ban decisions (cscli)
+ threat-intel C2 IPs (threat_intel ip IOCs) into the sets, 2h
per-element timeout (auto-expire on stale feeds), 50k cap, idempotent
add (no flush race with CrowdSec's own table).
- systemd sync .service + .timer (5min + OnBootSec 90s).
- postinst: install drop-in, reload nft, enable+start timer, first sync.
- api.py GET /admin/blacklist: element counts + drop counters (nft -j).
- debian/rules FIX: moved the whole override_dh_strip body into
execute_after_dh_auto_install. dh_strip is NOT in the binary-indep
sequence for an Architecture: all package, so override_dh_strip
silently never ran — the nft drop-ins / unbound / nginx / perf
drop-ins (and now the blacklist files) had stopped shipping in the
.deb and gk2 was running stale on-disk copies (the live-config-drift
we kept hitting). Now they all ship again.
- changelog 2.6.8.
Live on gk2: table loaded, first sync populated 18 v4 C2 IPs with 2h
timeouts, enforce chain active (4 drop rules, counters wired), timer
enabled, /admin/blacklist returns active:true.
The Clients-tab 🕸️ Carto link 303'd to a relative /social/{token}, but
that route is only served on the kbin vhost — from admin.gk2 it hit the
aggregator's 'missing module' page. admin_client_social() now swaps the
leading admin. host label for kbin. and redirects to the absolute URL.
Live: 303 -> https://kbin.gk2.secubox.in/social/{token} -> 200. changelog 2.6.6.
Anti-bot / 'prove you're human' DETECTION (bypass stays gated behind doctrine):
- social_graph.py detect_antibot(): reCAPTCHA/hCaptcha/Turnstile/Datadome/
PerimeterX-HUMAN/Arkose/Kasada/Akamai-BotManager from URL+cookies+headers.
- social.py: social_host_meta.antibot_vendor + social_antibot per-client
challenge table; fetch_graph/aggregate/wipe_mac carry it.
- social.js: cinnabar severe lens + spinning warning ring + 🤖 label +
'challenged your humanity' alert banner; node-detail vendor.
- operator social tab: anti-bot breakdown card.
Fix the ring-level visibility the user reported missing:
- radial force now DOMINANT (strength 0.9) with weak charge/links, plus
dashed ring guides (inner=sites, outer=trackers); assets cache-busted
?v=264b so the new layout actually loads.
Per-client operator tools in the Clients tab:
- 🕸️ Carto link → GET /admin/clients/{mac}/social mints a token + opens
that client's graph.
- ↺ Reset (RAZ) → POST /admin/clients/{mac}/reset wipes social + events +
consents + reports and zeroes score (store.reset_client + social.wipe_mac).
changelog 2.6.5. Live on gk2: both new tables created, detect_antibot matrix
green, aggregate by_antibot/antibot_clients present, reset endpoint 200, graph
nodes carry antibot_vendor, served assets contain the ring + cache-bust.
First passive track of the #514 anti-human-detection platform.
- social_graph.py: detect_cdn() classifies the edge network fronting
each 3rd-party host from response headers (Cloudflare/Fastly/Akamai/
CloudFront/Google/Vercel/Netlify/BunnyCDN/KeyCDN/Sucuri/Imperva +
generic edge-cache). Host-stable, recorded off-thread. Also deduped
a stray duplicate _ja4_hash left by the 11.C edit.
- social.py: new social_host_meta table; fetch_graph LEFT JOINs
cdn_vendor/cache_status onto nodes; aggregate adds by_cdn.
- social.js: per-client graph re-centred on a Round-Eye hotspot —
device = pulsing eye at centre, sites orbit on inner forceRadial
ring, trackers push to outer ring (densest clusters = hot spots).
Tracker nodes tinted by CDN vendor; node-detail shows CDN/cache.
- social.css: eye halo/sclera/iris/pupil + breathing anim + cinnabar
spokes.
- index.html: operator social tab gains a CDN/edge breakdown card.
- i18n: node_detail_cdn FR/EN. changelog 2.6.4.
Live on gk2: social_host_meta table created, addon loads detect_cdn,
detect_cdn matrix green (CF/Fastly/Akamai/CloudFront/none), end-to-end
read path proven (synthetic Cloudflare record -> graph node cdn_vendor
-> aggregate by_cdn). Populates from live traffic.
Checkpoint — backend evidence layer scaffolded. Frontend wire + PDF
generator + addon consent probe land in follow-up commits.
- social.py schema : social_edges.consent_state column ; social_nodes
gains country_iso / asn_org / eu_inside / pre_consent_hits.
- Idempotent _migrate_phase11c() ALTERs the pre-existing tables on
2.6.0 → 2.6.x upgrades (no destructive recreate).
- _EU_EEA_ISO whitelist (27 EU + 3 EFTA + UK adequacy) + is_eu_iso().
- _geo_for() LRU-cached (4096) wrapper around the existing geo
module ; fold time populates the GeoIP fields on every node row.
- record_edge() accepts consent_state (default 'none_seen') ; fold
accumulates pre_consent_hits into the per-node aggregate.
- evidence(mac_hash) helper : returns the two legal-grade buckets
(pre_consent + extra_eu) consumed by the PDF in the next commit.
Pivoting to admin tab routing per user request — Phase 11.C resumes
after that lands.
Consolidates the two toolbox admin surfaces onto the canonical
admin.gk2.secubox.in/toolbox/ WebUI.
- www/toolbox/index.html: 5-tab nav (Vue d'ensemble / Clients /
Filtres MITM / Cartographie sociale / Config). Lazy-load per tab,
live refresh only polls the visible tab, URL-hash deep-links.
Clients tab folds in the R0-R3 level switcher from the old kbin
admin UI. New Cartographie sociale tab surfaces the Phase 11
operator aggregate (KPI + top trackers + anonymized clients).
- api.py: DELETED the inline kbin /admin/ HTML route (admin_index,
~230 lines). All /admin/* JSON API routes preserved.
- conf/landing.html.j2: removed the Admin quicknav icon.
- debian/changelog: 2.6.1 -> 2.6.2.
Live on gk2: /toolbox/ 200 (19853 bytes, 5 tabs), kbin /admin/ now 404
by design, social-aggregate shows 225 trackers / 3 clients, all tab
endpoints (clients/rich, filter-control/list, social-aggregate) 200.
Live test screenshot showed 86 trackers / 60 sites computed correctly,
but only one tracker node was visible — the force layout had spread
the rest off-screen and the on-tick autoFit caught the simulation
mid-flight using SVG getBBox (which is sensitive to label text far
outside the cluster).
- Force tuning scales with node count (146 nodes → linkDist 40,
chargeStr -60, collide 14, alphaDecay 0.05 for faster settle).
- Pre-warm: 300 synchronous simulation.tick()s before first render so
positions are already converged.
- autoFit() uses node data positions (min/max x,y over nodes array)
instead of content.getBBox() — labels no longer skew the fit.
- First render: paint pre-warmed positions immediately, kick a gentle
alpha(0.05) restart for polish, autoFit on the next animation frame
with duration 0 (instant).
- Wrap graph content (links + nodes) in a content <g> so d3.zoom's
transform applies there without moving the SVG itself.
- d3.zoom(): scale 0.2–6×, mouse wheel + touch pinch + drag-pan.
Filter lets node drags pass through d3.drag (no collision with the
pan gesture) and always accepts multi-touch + wheel.
- autoFit(): after the simulation alpha drops below 0.1, compute the
nodes' bounding box and transition to a zoom transform that fits
all of them with 28 px padding (capped at 2.5× so a single-node
graph doesn't blow up). Same logic re-runs on dblclick (reset)
and on viewport resize (debounced 150 ms).
- Viewport-resize observer keeps the viewBox and zoom in sync when
orientation changes.
User feedback : graph too small + 'Chargement…' / 'ZÉRO TRACKER DÉTECTÉ'
overlays kept stacking on top of the graph.
Layout reworked :
- body flex column 100vh overflow hidden — header sticky, main owns
all remaining vertical space, graph fills it.
- stats tiles → small glassy pill floating top-left over the graph.
- cards collapsed into a compact 3-cell horizontal strip at the
bottom ; open-card popover slides up over the graph so the graph
stays visible.
- graph svg measured at render-time, viewBox set dynamically — d3
force-center scales to actual viewport instead of the fixed
600×600 box.
Persistent overlays removed :
- .social-empty and .social-loading divs gone from template.
- JS no longer references them. Empty graph is its own indicator
(stats tiles show 0/0). Network errors log to console only.
Net effect : tap 'Ma carto' on splash → full-screen force-directed
graph, stats top-left, cards bottom strip, no overlay clutter.
JSON in a single-quoted HTML data-* attribute breaks on FR apostrophes
(l'effacement, L'analyse) — JSON.parse threw 'unterminated string at
line 1 column 475'. <script>window.__SOCIAL_I18N__ = {…}</script>
keeps the JSON verbatim and is the standard idiom for shipping
server-rendered config to client JS.
Root cause caught after Phase 11.B live deploy : the mitm-wg workers
run inside /opt/mitmproxy-toolbox/bin/python3 — a venv whose
site-packages does NOT include /usr/lib/secubox/toolbox/. Every addon
that does `from secubox_toolbox import ...` was therefore silently
degraded :
- inject_banner.py: dpi_class / geo / store / utiq imports all
failed → no host classification, no GeoIP, no ASN, no Utiq tile.
- social_graph.py: _social = None → record_edge() no-op → zero
social_edges rows even though the response hook fired correctly.
Adding `export PYTHONPATH=/usr/lib/secubox/toolbox` to the launcher
restores ALL helper imports for ALL addons.
Verified live on gk2 : after deploy + worker restart, 369 social_edges
recorded in 30s, fold produced cross-site links connecting 4 ad-tech
relay publishers (360yield, seedtag, smartadserver, smilewanted via
the same 35.214.136.108 endpoint).
iPhone hitting /social/me from the kbin splash on R3 tunnel was 400'ing
because the previous chain was only ?mh=<hash> + ARP _resolve(), and R3
peers are on 10.99.1.0/24 not the captive subnet.
New 3-step resolution chain (matches /report/me/html):
1. ?mh=<hash> — explicit hex hash in URL
2. X-R3-Peer header — sentinel set by mitm-wg's inject_xff
for transparent R3 flows ; we look up
the WG pubkey hash from
/var/lib/secubox/toolbox/wg-peers.json
keyed by peer IP.
3. ARP _resolve() — R2 captive subnet fallback
Verified live: GET /social/me with X-R3-Peer: 10.99.1.60 → 303 →
/social/<token-for-mac_hash 9433ceb90895a075>
Five fixes caught during the first live deploy on gk2 :
1. mitmproxy_addons/social_graph.py: `from . import local_store` was
never resolving because mitmproxy loads addons as top-level
modules (not as package members). Inlined the R3 path
(10.99.1.0/24 → sha256(wg_pubkey)[:16]) directly. Verified
against the real iPhone IP (10.99.1.60 → 9433ceb90895a075).
2. www/toolbox/social.js: the JSON fetch + wipe paths used the
nginx-prefixed `/api/v1/toolbox/social/...`. The kbin vhost
routes via HAProxy straight to uvicorn (bypassing nginx) so
that prefix never matches. Rewrote to the FastAPI router
paths `/social/graph/{token}` + `/social/wipe/{token}`.
3. secubox_toolbox/app.py: FastAPI StaticFiles mount on /toolbox
for the kbin HAProxy path (which doesn't go through nginx).
The mount serves the same files nginx would alias from
/usr/share/secubox/www/toolbox/.
4. debian/postinst: chmod 0755 /usr/share/secubox/www so the
`secubox-toolbox` service user (not in secubox group) can
traverse to the WebUI directory. Without this the StaticFiles
mount crashed the service at startup with PermissionError.
5. New /social/me endpoint + splash menu icon : self-resolving
entry point that mints a 1 h HMAC token and 303-redirects to
/social/{token}. Mirrors the existing /report/me/html pattern.
Splash page gets a 🕸️ "Ma carto" quick-nav icon.
End-to-end via gk2 live :
/social/me?mh=<hash> → 303 → /social/{token} → 200 (4883 bytes FR)
static assets : 200 / 7529 / 8359 / 279706 bytes
addon smoke test : mac_hash for 10.99.1.60 → 9433ceb90895a075 ✓
Parent #502 (Phase 11 social mapping per device). Builds on top of
the Phase 11.A backend (PR #506, branch feature/505).
- secubox_toolbox/api.py: GET /social/{token} renders the HTML view
(HMAC-token gated, FR/EN i18n via ?lang= or Accept-Language).
GET /social/favicon/{domain} server-side cached proxy (7d TTL,
strict charset, HTTPS only, hard 5s timeout, transparent gif
fallback).
- conf/social_view.html.j2: Jinja2 template implementing the
locked design from the #502 round-2 lock — Cinzel header / IM
Fell body / JetBrains Mono data, force-directed d3 graph, three
collapsible cards, tracker-node bottom-sheet, wipe dialog with
3 s countdown.
- conf/i18n/social.{fr,en}.json: single dictionary per language.
FR is the source-of-truth.
- www/toolbox/social.css: palette-precise stylesheet. Mobile-first;
desktop bumps the node-detail panel to a right-rail at >=720 px.
- www/toolbox/social.js: self-contained d3 driver. Consumes the
Phase A JSON contract, force layout (link distance 70, charge
-180), tap-to-focus pulse on tracker nodes, wipe modal countdown
timer, refresh after wipe.
- www/toolbox/d3.v7.min.js: d3 v7.9.0 self-hosted (~280 KB),
ISC-licensed, no CDN runtime dependency.
- debian/changelog: bump to 2.6.1-1~bookworm1.
Smoke tests: FR + EN templates render (4771 / 4503 bytes), api.py
parses, social.js node-checks clean.
Phase C scope deferred — legal-evidence flag computation, bilingual
PDF report, GeoIP+ASN populated values on the node detail panel,
operator dashboard /admin/social/ HTML view (the JSON
/admin/social-aggregate endpoint already ships from 2.6.0).
Caught during 2.6.0 live deploy on gk2: `debian/rules` had install lines
for `sbin/secubox-toolbox-db-tune` (override_dh_strip) and a few wg-
phase scripts but NEVER for `sbin/secubox-toolbox-mitm-wg-launch`. The
existing launcher on gk2 was a leftover from an older deploy and dpkg
upgrades silently kept the file static — which masked the Phase 11.A
addon-chain edit (social_graph was added in source but never reached
the live launcher).
- debian/rules: explicit install lines for the three sbin helpers
(lxc-provision, mitm-wg-launch, wg-provision). These were either
ad-hoc-installed before or relied on the operator copying them
by hand.
- sbin/secubox-toolbox-mitm-wg-launch: chmod 0755 (was 0664).
- sbin/secubox-toolbox-wg-restore: chmod 0755 (was 0664).
Closes a source-first regression caught by [feedback_source_first_always].
Parent #502 (Phase 11 social mapping per device). Backend-only deliverable
unblocking Phase 11.B (d3 view) and 11.C (legal-evidence PDF).
- secubox_toolbox/social.py: new module. SQLite schema for
social_edges (raw, 7d retention) + social_nodes (per-peer per-tracker
aggregate) + social_links (per-peer per-site-pair with shared-tracker
list and JA4 collision flag). ThreadPoolExecutor fire-and-forget
write path matches utiq.py. fold_recent() runs every 5 min and
derives the aggregate tables. wipe_mac() backs the RGPD art. 17
endpoint. aggregate() backs the operator dashboard.
- mitmproxy_addons/social_graph.py: new addon. Loaded after
local_store and before inject_banner in the mitm-wg chain.
Parses Set-Cookie + Cookie headers, normalizes via
cookie_id_hash = sha256(domain||name||value)[:16] — raw cookie
values are NEVER persisted. Deny-list strips
session/CSRF/auth/locale cookie names. Cheap eTLD+1
approximation for 1st- vs 3rd-party classification. Reuses
local_store helpers for the rotating MAC hash.
- secubox_toolbox/api.py: three new endpoints.
GET /social/graph/{token} per-client graph (HMAC token)
POST /social/wipe/{token} RGPD art. 17 droit à l'effacement
GET /admin/social-aggregate operator dashboard KPI view
- secubox_toolbox/app.py: two new startup background tasks —
social_fold_loop (5 min) and social_purge_loop (1 h). Match the
existing purge/threat_intel pattern.
- sbin/secubox-toolbox-mitm-wg-launch: addon chain wired
(… local_store → social_graph → inject_banner …).
Phase A scope locks: R3 mitm-wg path only. R2 captive hookup, d3
graph view, bilingual FR/EN PDF report, and the consent-state /
extra-EU legal-evidence flags are deferred per the #502 design lock
(rounds 1 + 2).
Smoke-tested on the host : hash stability, deny-list, edge insertion,
fold, fetch_graph contract shape, aggregate, wipe_mac round-trip,
addon helpers (eTLD+1, Set-Cookie parser, Cookie parser, Domain attr).
All seven invariants green.
- debian/changelog: bump to 2.6.0-1~bookworm1.
Shared-directory ownership race: secubox-toolbox + secubox-mesh created
/var/log/secubox at 0750 with their own module owner, secubox-admin at
750 root:secubox, while ~10 other packages use the correct 0755
secubox:secubox. Whichever postinst ran last won — and when toolbox or
mesh won, the aggregator (user secubox) lost traversal and the /waf/ +
/soc/ dashboards went blank (regressed on gk2 2026-06-10).
- secubox-toolbox/debian/postinst:47 0750 -> 0755 (+ guard comment)
- secubox-mesh/debian/postinst:23 0750 -> 0755
- secubox-admin/debian/postinst:11 750 -> 0755
Mode 0755 makes the shared parent world-traversable regardless of which
package's owner wins. Per-module log files + subdirs inside keep their
own restricted perms. Same class as the /etc/secubox traversal
constraint + the /usr/share/secubox/www chmod (#507).
User reported /waf/ + /soc/ dashboards showing empty threats and tracked
attackers cards. Live triage on gk2 found _get_threat_stats() iterating
all 332k JSONL entries in the 110 MB waf-threats.log on every request,
pinning the aggregator at 89% CPU with 8+ concurrent open handles.
- api/main.py: _load_stats_disk_cache + _save_stats_disk_cache helpers
persist counters + last-read byte position to
/var/lib/secubox/waf/stats-disk-cache.json (atomic .tmp -> rename so
a crash mid-write never corrupts).
- api/main.py: _get_threat_stats reseats the file at the saved byte
position, reads only the new tail since then, persists the new
position before returning.
- api/main.py: log rotation / truncation detected via size shrink;
counters reset cleanly.
- api/main.py: day rollover resets threats_today only; cumulative
counters survive.
- api/main.py: ip_countries dict capped at 1200 entries (most-active
1000 retained on overflow) so the cache file stays small.
Live on gk2 after deploy: /waf/stats steady-state 30-37 ms (vs 30s+
timeout before). /waf/alerts, /waf/bans/history, /soc/* all 200 in
under 200 ms. Aggregator CPU drops from 89% to 52% under same load.
- debian/changelog: 1.2.1 -> 1.2.2-1~bookworm1.