Commit Graph

1702 Commits

Author SHA1 Message Date
0aeedcf1f4 feat(toolbox): d3 pan + pinch-zoom + auto-fit initial render (ref #507)
- 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.
2026-06-10 14:22:16 +02:00
8a4598e2b3 feat(toolbox): full-viewport graph + drop persistent overlay messages (ref #507)
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.
2026-06-10 14:22:16 +02:00
720d61a9d3 fix(toolbox): move social-view i18n from data-* attr to <script> (ref #507)
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.
2026-06-10 14:22:16 +02:00
d195408fd4 fix(toolbox): PYTHONPATH in mitm-wg launcher so addons can import secubox_toolbox (ref #507)
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).
2026-06-10 14:22:16 +02:00
8dc1449eab fix(toolbox): /social/me R3 transparent peer resolution via X-R3-Peer (ref #507)
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>
2026-06-10 14:22:16 +02:00
fc22b64b99 fix(toolbox): Phase 11.B live-deploy hardening — addon import, static mount, kbin paths, splash link (ref #507)
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 ✓
2026-06-10 14:22:16 +02:00
693ee360e9 feat(toolbox): Phase 11.B social mapping per-client view + favicon proxy + i18n (ref #507)
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).
2026-06-10 14:22:16 +02:00
bafd856d1e fix(toolbox): debian/rules ships sbin/* launchers consistently (ref #505)
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].
2026-06-10 14:22:16 +02:00
6b230d464d feat(toolbox): Phase 11.A social mapping backend — correlation engine + API (ref #505)
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.
2026-06-10 14:22:16 +02:00
2b1d2173c0 docs: add Cartographie sociale 'You Have Been Tracked' README section + poster brief (ref #502)
Some checks are pending
License Headers / check (push) Waiting to run
- README: new VILLAGE3B subsection for the Phase 11 social mapping
    feature (per-client /social/me, d3 graph, RGPD wipe, operator tab).
  - docs/marketing/POSTER-you-have-been-tracked.md: poster brief with
    the real kbin metrics (218 trackers, 142 sites, 234 JA4, 83693
    connexions) + doctrine guardrails + declension variants.
2026-06-10 12:52:36 +02:00
CyberMind
4339590eb1
Merge pull request #512 from CyberMind-FR/fix/511-fix-var-log-secubox-traversal-converge-o
Some checks are pending
License Headers / check (push) Waiting to run
fix: /var/log/secubox traversal — converge postinsts to 0755 (#511)
2026-06-10 09:59:14 +02:00
13faf609f7 fix: converge /var/log/secubox postinsts to 0755 traversable (closes #511)
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).
2026-06-10 09:53:48 +02:00
8263bc7681 docs: update WIP/HISTORY/TODO for Phase 11 A+B + gk2 system triage + v2.13.14
Some checks are pending
License Headers / check (push) Waiting to run
- Phase 11 social mapping A (#506/2.6.0) + B (#507/2.6.1) deployed live;
    C (#508) checkpointed at 55626e51.
  - gk2 triage: CrowdSec bouncer tables, /var/log/secubox traversal,
    WAF double-buffer cache (#509/1.2.2), PeerTube/PhotoPrism restart.
  - CI: espressobin disable (#504), WAF perf (#510) merged, v2.13.14 tagged.
  - Carried: Round Eye link, toolbox tab decision, var/log postinst patch.
2026-06-10 08:44:55 +02:00
CyberMind
a6f44807c6
Merge pull request #510 from CyberMind-FR/fix/509-waf-soc-perf-double-buffered-cache-memor
perf(waf): double-buffered cache for /waf/stats + /soc (#509)
2026-06-10 08:42:12 +02:00
CyberMind
3ebb4477cc
Merge pull request #504 from CyberMind-FR/fix/503-ci-disable-espressobin-v7-ultra-image-bu
CI: drop espressobin-v7 + ultra from scheduled image matrix (#503)
2026-06-10 08:42:09 +02:00
b7666e5f57 perf(waf): double-buffered cache for /waf/stats (memory + disk) with incremental log reading (closes #509)
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.
2026-06-10 08:39:57 +02:00
3d52b63d26 ci: drop espressobin-v7 + espressobin-ultra from scheduled image matrix (ref #503)
The two boards fail in the cross-arm64 chroot stage of build-image.yml
and (even with fail-fast: false) block the downstream release.yml job
from publishing the OTHER boards' images.  Releases v2.13.9 through
v2.13.12 all hit this trap.

This change keeps both entries available via workflow_dispatch (operators
can still build them on-demand), but removes them from the push-tag
scheduled matrix so mochabin / vm-x64 / rpi400 actually ship on tag.

Board support files (board/espressobin-*/, image/build-image.sh
--board espressobin-*) stay in tree.

  - build-image.yml matrix excludes espressobin-v7 + espressobin-ultra
    on push and workflow_call.
  - build-image.yml workflow_dispatch choice list keeps both entries
    flagged as on-demand only.
  - Release notes template drops the two image rows, adds a note
    explaining the on-demand path.
  - Install instruction adjusted (MOCHAbin + Raspberry Pi 400).
2026-06-09 09:42:08 +02:00
CyberMind
2b9a060561
Merge pull request #501 from CyberMind-FR/perf/501-banner-injection-quickwins
Some checks are pending
License Headers / check (push) Waiting to run
Perf/501 banner injection quickwins
2026-06-09 07:50:43 +02:00
53e819cd2d docs: update WIP/HISTORY/TODO for Phase 10 banner perf + postinst fix (ref #501)
- WIP.md: new 2026-06-09 entry covering 2.5.1 (banner perf) + 2.5.2
    (postinst regression fix) with diagnosis, packages, memory links
    and next-up.
  - HISTORY.md: companion timestamped entry with live mitigations applied
    on gk2 today.
  - TODO.md: mark Phase 10 + Phase 9 shipped in P0; remove obsolete
    "Phase 9 (futur)" dispatcher block (resolved by nft numgen fanout).
2026-06-09 07:14:26 +02:00
15f48d9d4b fix(toolbox): postinst preserves Phase 9 fanout + try-restart on upgrade (ref #501)
Two silent regressions surfaced during the 2.5.0 → 2.5.1 deploy on gk2:

  1. kbin.gk2.secubox.in 503 for 5 min after upgrade.  dpkg SIGTERMed
     secubox-toolbox.service and never restarted it because
     dh_installsystemd --no-start --no-enable is set in debian/rules.

  2. iPhone tunnel went from ~10 s/page to unusable.  postinst overwrote
     the operator-deployed fanout nft drop-in
     (/etc/nftables.d/secubox-toolbox-wg.nft) with the pristine
     single-port-DNAT version, pinning all R3 traffic to worker@1
     (97 % CPU while w2-w4 sat idle).

Fixes — postinst only, no code change:

  - debian/postinst: ALWAYS install the Phase 9 fanout drop-in as
    /etc/nftables.d/zz-secubox-toolbox-wg-fanout.nft.  zz- prefix sorts
    after the base file in /etc/nftables.conf's alphabetical glob
    include, so the base file creates the table + chains and the zz
    drop-in then flushes the prerouting chain and repopulates it with
    the numgen mod 4 map across ports 8081..8084.

  - debian/postinst: on UPGRADE ($2 set), try-restart the long-running
    units (secubox-toolbox, secubox-toolbox-mitm, and the 4 worker
    instances).  try-restart is a no-op when the unit isn't running,
    so fresh installs / unconfigured boards are unaffected.

  - debian/changelog: bump to 2.5.2-1~bookworm1.
2026-06-09 07:11:07 +02:00
ce059d0f9d perf(toolbox): banner injection quick wins, drop body scan + LRU host cache (ref #501)
User reported the banner only appeared at end-of-load and pages were
very slow.  Three host-stable lookups (classify_host, whitelist match,
GeoIP) ran on every flow without caching, and a full-body regex scan
ran just to count "trackers in body" for a tile most users don't read.

  - inject_banner.py: new _host_signals(host) returning a 7-tuple,
    cached @functools.lru_cache(maxsize=2048).  Re-hit cost goes
    from 5-50 ms to a dict lookup.
  - inject_banner.py: drop _count_trackers_in_body() call from the
    response hot path.  is_tracker_host flag (cheap host regex)
    keeps the privacy signal.
  - inject_banner.py: _MAX_INJECT_BYTES = 2 MB cap on response()
    via Content-Length pre-flight + defensive len() guard.
  - inject_banner.py: drop the now-dead "🎯 N body trackers" tile
    branch ; the cookie + ⚠ tracker-host tiles stay.

Net: banner shows at first paint, TTFB on big HTML responses no
longer banner-bound.

  - debian/changelog: bump to 2.5.1-1~bookworm1.
2026-06-09 06:40:42 +02:00
89380a121a Merge feature/501-mitm-wg-multi-worker-fanout : Phase 9 mitm-wg multi-worker fanout (ref #501)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-09 06:27:40 +02:00
c17810e1f0 feat(toolbox): Phase 9 multi-worker fanout for mitm-wg (ref #501)
The single-process mitm-wg saturated one ARM core at ~90 % under just
2-3 active wg peers because the Python GIL caps real parallelism
inside one mitmproxy process.  No single-process tweak moves that
needle further than Phase 8.1 already did (CPU 65 % → 12 % at idle ;
at multi-peer load we're back to 90 %+).

Phase 9 ships 4 mitm worker instances and lets nft round-robin
distribute new TCP flows across them via `numgen inc mod 4`.
Conntrack pins each flow's DNAT translation for its lifetime, so a
given TCP connection sees exactly one worker from SYN to FIN —
sticky-per-flow without needing nftables 1.0.7+ jhash support
(Debian bookworm ships 1.0.6).

What's shipped :

  systemd/secubox-toolbox-mitm-wg-worker@.service
    Template unit ; per-instance Environment=MITM_WG_LISTEN_PORT=
    808%i (8081..8084).  Per-worker RuntimeMaxSec=3h, MemoryMax=128M,
    TasksMax=128, User=secubox-toolbox.

  sbin/secubox-toolbox-mitm-wg-launch
    Now reads MITM_WG_LISTEN_PORT (default 8081 for the legacy
    single-worker service).

  nftables.d/secubox-toolbox-wg-fanout.nft
    Replaces the single-port DNAT rules with a numgen-inc round-robin
    map to 4 ports.  DNS + captive-portal DNAT rules stay untouched
    (small queries, no benefit from fanout).

  debian/rules
    Installs both the worker template and the fanout nft drop-in
    next to the existing single-worker artifacts.

Activation (operator-initiated) :

  systemctl disable --now secubox-toolbox-mitm-wg.service
  systemctl enable --now secubox-toolbox-mitm-wg-worker@{1,2,3,4}.service
  ln -sf /usr/share/secubox/toolbox/nftables.d/secubox-toolbox-wg-fanout.nft \
         /etc/nftables.d/secubox-toolbox-wg.nft
  nft -f /etc/nftables.d/secubox-toolbox-wg.nft

Rollback : reverse the steps above ; the legacy single-worker
service and its single-port nft drop-in remain shipped + functional.

Live numbers on gk2 with the workload active during the cut-over
(2 Linux peers + 1 iPhone, dozens of concurrent flows) :

  before  one process @ 90-95 % CPU on a single core, saturated
  after   ~55 % avg per worker × 4, 0-70 % range, all cores below saturation

The Phase 8.B addon-write fire-and-forget pattern shipped in 2.4.3
becomes load-bearing here : SQLite WAL on toolbox.db handles 4
concurrent writers cleanly because each worker's _executor serialises
its own writes, and SQLite's writer mutex handles the inter-worker
contention with no event-loop stalls.

Known limitation : the cert-pin auto-learning dynamic bypass file is
the remaining race surface (4 writers can dupe a line under burst,
the launcher's sort -u de-dupes at next reload).  A real filelock
lands in Phase 9.1.
2026-06-09 06:27:27 +02:00
06e73d39bd Merge perf/500-captive-flags-and-addon-async : captive flags + addon async writes (ref #500) 2026-06-09 06:19:30 +02:00
d3fbf174c0 perf(toolbox): captive mitm flag symmetry + addon writes off-thread (ref #500)
A — Captive mitm-toolbox (secubox-toolbox-mitm.service) gets the same
three flags as mitm-wg picked up in 2.4.1 :

  --set http2=true
  --set connection_strategy=eager
  --set keep_host_header=true

The captive idles at ~0 % CPU right now (wlan AP is down), so no
visible change today.  When the AP is reactivated the captive will
inherit the same ×4 CPU win the WG path saw — and it stays in
symmetry with mitm-wg so future tweaks land on both.

B — Addon SQLite writes are now fire-and-forget via singleton
ThreadPoolExecutor (max_workers=1, thread_name_prefix=…) :

  local_store._insert  → submitted to sbx_store_write
  utiq.record_event    → submitted to sbx_utiq_write

The hook returns instantly ; the _conn() open + INSERT + fsync chain
runs on the bg thread.  No more event-loop stalls during peer flow
processing.

Live diagnostic on gk2 surfaced the actual bottleneck while shipping
this : one Linux PC peer (10.99.1.60) was generating ~3 client
connects/sec sustained, the second Linux PC (10.99.1.47) running in
parallel, and mitm-wg processing ~100 concurrent TLS sessions.  At
that level the CPU is consumed by mitmproxy itself (TLS termination
+ per-flow H/2 stream parsing under the Python GIL), NOT by the
addon writes — so A and B don't move the needle today.

They remain shipped as defensive hygiene before Phase 9 multi-worker
fanout : when 4 mitm workers contend on the same SQLite file, the
fire-and-forget pattern matters.
2026-06-09 06:19:19 +02:00
678adc8f68 Merge feature/500-landing-platform-install : landing kbin embeds platform-detected install panels (ref #500)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-08 17:51:53 +02:00
b154894278 feat(toolbox): landing kbin embeds platform-detected install panels (ref #500)
The kbin.gk2.secubox.in landing page had a hand-written ordered list
showing only the iOS install steps.  Visitors on Android / Linux /
macOS / Windows had to follow a generic flow then go elsewhere to
get their proper artifacts.

Refactored : the install section now renders the same 5 platform
panels as /wg/onboard, with server-side UA sniffing to open the
right panel first.  Each panel keeps the same content (App Store
links / QR / .conf / .nmconnection / .mobileconfig / install
commands per OS).

Implementation : extracted _install_panels_html() from the prior
_render_onboard() so both endpoints render identical markup.  CSS
classes stay landing-local (a fresh <style> block) so the existing
landing design language is preserved.  No new endpoints ; the
landing.html.j2 receives `install_panels` + `install_platform`
context.

Visitor flow now : open kbin.gk2.secubox.in → "📥 Installer R3 sur
ton appareil" → already-open panel matches the visitor's device →
single click on the install button or QR scan.

Live verified on gk2 : curl with iPhone UA renders `id="install-ios"
open` first ; Linux UA renders `id="install-linux" open` first ;
HTTP 200, 28 KB rendered page.
2026-06-08 17:51:43 +02:00
3b4a185775 Merge perf/500-mitm-wg-h2-and-concurrency : mitm-wg CPU 65→12% via H/2 + eager + keep-alive (ref #500)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-08 17:39:56 +02:00
f9fd8ea333 perf(toolbox): mitm-wg CPU 65 % → 12 % via HTTP/2 + eager + keep-alive (ref #500)
Live diagnostic on gk2 showed the host mitm-wg process pegged at
65 % single-core CPU at near-zero throughput, with 35 enrolled peers
and 3 active.  Root cause is the single Python asyncio event loop
running 10 addons per flow ; Phase 6.P disabling HTTP/2 and Phase 6.J
forcing Connection:close upstream were the right calls at the time
(memory leaks in older mitmproxy) but their cost has been CPU-bound
since the mitmproxy 11 upgrade.

Re-enabled three settings in the launcher :

  --set http2=true               — multiplex halves TLS handshakes
  --set connection_strategy=eager — overlaps upstream RTT with
                                    downstream parse
  --set keep_host_header=true    — upstream keep-alive (mitmproxy 11
                                    fixed the leak)

To compensate the H/2 in-process state drift, the
service.d/10-runtime-max.conf drops the recycle from 6 h to 3 h.
Memory still bounded by the 400 MB envelope ; restart latency stays
sub-second so no user-visible impact.

Live verified : CPU 65 % → 12 % at the same workload level. Banner
injection + utiq_defense addon still firing correctly.  iPhone tunnel
+ Linux PC tunnel both functional through the restart.

Multi-worker fanout (true horizontal scaling — N mitmproxy workers
sharded by nft DNAT hash) tracked as Phase 9 in TODO ; the
single-process quick win above is enough headroom for the current
~35 enrolled peers, deferring the worker pool work.
2026-06-08 17:39:44 +02:00
a552d842ab Merge feature/500-phase8-utiq-quickwin : Phase 8 anti-Utiq R0+R1 Quick Win (ref #500)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-08 16:20:54 +02:00
41ce31ff38 feat(toolbox): Phase 8 Quick Win — anti-Utiq R0 + R1 defense (ref #500)
Operator-grade tracking detection / blocking for the Utiq consortium
(Deutsche Telekom + Orange + Telefónica + Vodafone, launched 2023).
Utiq issues a 90-day-stable mtid identifier per publisher based on
SIM + IP carrier-level auth — cookies and standard anti-fingerprint
defenses don't help.

Quick Win ships R0 + R1 :

  R0 (log)    — every flow involving *.utiq.com or utiqLoader.js is
                stored in /var/lib/secubox/toolbox/toolbox.db so the
                operator can audit. No effect on the flow.
  R1 (block)  — same detection, but flow.response is short-circuited
                to 451 (Unavailable For Legal Reasons) before the
                upstream is contacted. No mtid is ever revealed to
                the publisher. Some pages may degrade — the worst
                case is no targeted ad. Toggle globally via env var
                UTIQ_DEFAULT_LEVEL=R1.

Per-client toggling and R2 (mask) / R3 (pseudo-avatar) ship in Phase 2.

Files :

  mitmproxy_addons/utiq_defense.py
    - hooks requestheaders (short-circuits before body fetched)
    - matches *.utiq.com (host) + utiqLoader.js (path, for 1st-party
      CNAME wrappers)
    - records via secubox_toolbox.utiq.record_event

  secubox_toolbox/utiq.py
    - utiq_events table : id, ts, client_ip, publisher (extracted
      from host), host, path, action, level, detected_mtid (future),
      injected_mtid (future)
    - indexes on ts, (client_ip, ts), (publisher, ts)
    - recent() / aggregates() / client_recent_count() helpers

  secubox_toolbox/api.py
    - GET /admin/utiq-events?hours=&limit= → events + aggregates

  mitmproxy_addons/inject_banner.py
    - new ctx['utiq_recent_count'] from utiq.client_recent_count
    - '📡 utiq:N' tile appears when count > 0

  sbin/secubox-toolbox-mitm-wg-launch
    - utiq_defense inserted between inject_xff and local_store

Live verified : addon loaded, /admin/utiq-events returns empty events
+ aggregates skeleton, no regressions on the existing addon chain.

Closes the Quick Win checkbox in #500.
2026-06-08 16:19:51 +02:00
df822062f7 fix(toolbox): unbound listener for br-lxc + auto-resolve from any LXC (ref #498)
Some checks are pending
License Headers / check (push) Waiting to run
Phase 7.E.3 follow-up — every LXC on br-lxc (10.100.0.0/24) had
DNS=10.100.0.1 from its static config but the host Unbound only
listened on 10.99.x (the WG subnets shipped in Phase 7.E.1 by
99-secubox-wg.conf). External DNS from inside any LXC failed
silently.

Symptom found 2026-06-08 : the Gitea pull-mirror that lives in the
gitea LXC was failing to sync from upstream
github.com/CyberMind-FR/secubox-deb with :

  Stderr: fatal: unable to access
    'https://github.com/CyberMind-FR/secubox-deb.git/':
    Could not resolve host: github.com

Failure dated back to 2026-05-17 (the day before the last successful
sync). No retry alert because the mirror is "schedule, log, move on"
and Gitea doesn't promote DNS errors to dashboard warnings. The
Gitea mirror operator only noticed because someone checked the repo
was 3 weeks behind master.

Fix : new unbound drop-in 98-secubox-lxc.conf listens on 10.100.0.1
with access-control 10.100.0.0/24 allow. Postinst now installs both
drop-ins (wg + lxc) in one pass.

Live verified : `getent hosts github.com` from inside gitea LXC
returns 140.82.121.4 ; the next mirror tick (10 min for
secubox/secubox-deb, 8 h for gandalf/secubox-deb) catches up the
~50 missing commits automatically.
2026-06-08 12:08:49 +02:00
2585fe5577 Merge fix/498-crowdsec-sudoers-status : extend sudoers for crowdsec /status + /console/status (ref #498) 2026-06-08 12:00:56 +02:00
5cdb3be557 fix(crowdsec): sudoers covers /status + /console/status read-only cscli (ref #498)
After today's `cscli console enroll …` the SecuBox CrowdSec dashboard
still showed "CAPI Not enrolled" + "LAPI not reachable" — even with
`cscli capi status` confirming successful CAPI auth from a root shell.

Root cause : `/api/v1/crowdsec/status` shells out to
  sudo cscli lapi status
  sudo cscli capi status
  sudo cscli decisions list -o json
  sudo cscli alerts list -o json
plus `/api/v1/crowdsec/console/status` calls
  sudo cscli console status

The 1.0.6 sudoers.d drop-in only listed `cscli bouncers list` and
`nft list tables` — the 5 above had no NOPASSWD entry. Under the
aggregator user `secubox` they triggered sudo's password prompt,
returncode != 0, and the FastAPI mapped them all to `false`.

This drop adds the missing 5 read-only commands. All exact-match, no
wildcards, no shell — residual capability stays bounded to read-only
listings.

Verified live on gk2 : the /status endpoint now returns
  running:        True
  lapi_reachable: True
  capi_registered: True
  decisions_count: 6
  alerts_count:    50
2026-06-08 12:00:45 +02:00
18502b29c2 docs: WIP + HISTORY + TODO for Phase 7.E.x + Phase 8 opening (ref #498, #500)
Tracking files mis à jour pour la journée du 2026-06-08 :

  WIP.md     — section Phase 7.E.x avec bumps secubox-waf 1.2.1,
                secubox-toolbox 2.3.2, secubox-auth 1.0.2,
                secubox-users 1.4.2 + finding architectural wg-LXC
                + plan Phase 8 Utiq.
  HISTORY.md — entry datée avec détail des 6 axes shippés
                (LXC WAF hygiene, /wg/onboard, NM name fix, LXC wg
                scaffolding, auth recovery, Phase 8 ouverture).
  TODO.md    — backlog Phase 8 Quick Win/Phase 2/Phase 3 + follow-ups
                7.E (rebuild + dpkg upgrade live secubox-aggregator
                0.2.1 et secubox-users 1.4.2) + Phase 9 (futur multi-
                instance mitm-wg dispatcher).

Wiki Phase-7-Roadmap.md mis à jour en parallèle dans
secubox-deb.wiki@1610d70.
2026-06-08 11:41:03 +02:00
64e4de16d3 Merge fix/498-auth-perms-and-rw : postinst chowns auth.toml (ref #498) 2026-06-08 10:10:04 +02:00
d476346ef4 fix(users): postinst also chowns auth.toml (ref #498)
After Phase 7.D consolidation, the aggregator runs as user `secubox`.
secubox_core.user_store reads BOTH /etc/secubox/users.json and (as
fallback) /etc/secubox/auth.toml. The secubox-users postinst chowned
users.json to root:secubox 0640, but auth.toml had NEVER been touched
by any postinst — left at the package default 0600 root:root since the
secubox-auth 0.1.0 install in May 2026.

Live symptom : every admin login returned "Identifiants incorrects"
no matter what password was typed. The aggregator log showed :

  user_store: fallback to auth.toml for get_user(admin)
  user_store: auth.toml unreadable ([Errno 13] Permission denied:
              '/etc/secubox/auth.toml')

The fallback path was the only reason auth.toml mattered at all (the
canonical store is users.json) — but with users.json reset to root:root
by an earlier intervention, the fallback became the active path and
its permission denial took down the entire auth chain.

This commit ensures both files are root:secubox 0640 after every
package install / upgrade.
2026-06-08 10:09:50 +02:00
87bd8e5137 Merge fix/498-ntp-health-timesyncd : ntp_health timedatectl fallback (ref #498) 2026-06-08 09:50:02 +02:00
260f0f5d6d fix(auth): ntp_health falls back to timedatectl when chronyc absent (ref #498)
Admin login UI was permanently flagging "Clock not synced" with the
warning "TOTP window widened to ±60s" even when the system clock was
correctly synced. Cause : ntp_health.probe() only knew how to read
chrony, but SecuBox boxes ship systemd-timesyncd by default ; chrony
is the operator-opt-in alternative. On a stock install the probe
caught FileNotFoundError, returned `synced: False`, and the
recommended_totp_window() widened to ±60s on EVERY login.

Added a chrony → timedatectl fallback chain :

  1. Try `chronyc -n tracking` (chrony users).
  2. Fall through to `timedatectl show` reading NTP +
     NTPSynchronized properties (timesyncd users).
  3. Only return the synthetic error if BOTH commands are missing.

The response now carries an extra `source` field
("chrony" / "timedatectl") so the operator can see which backend
answered the probe.
2026-06-08 09:49:51 +02:00
3c4d1cc1d3 Merge feature/498-lxc-wg-privileged : wg LXC privileged + listen-host env + netns finding (ref #498) 2026-06-08 09:43:59 +02:00
c0fc354597 fix(toolbox): wg LXC privileged mode + listen-host env var, netns finding (ref #498)
Continuation of the wg LXC migration. Resolved the unprivileged-LXC
bind-mount denial that blocked 2.3.1 by switching the wg target to
PRIVILEGED LXC (matches the existing mitmproxy WAF LXC convention),
and parameterised the launcher's listen-host so the LXC can bind
0.0.0.0 on its br-lxc interface.

Live cutover attempt then surfaced a deeper architectural issue :

  mitmproxy transparent mode uses Linux's SO_ORIGINAL_DST socket
  option to recover the original destination IP/port. SO_ORIGINAL_DST
  is conntrack-backed, and conntrack is namespace-scoped. The DNAT
  entries created by the host's `nft prerouting` chain are invisible
  inside the LXC's netns. Every flow errors with :

      Transparent mode failure: FileNotFoundError(2, 'No such file or
      directory')

  and gets RST'd after the TLS ClientHello.

Two viable architectures going forward, documented inline in the
LXC conf template :

  1. Keep mitm-wg on the host (current path — the host service was
     never disabled in source, only at runtime, and the rollback
     re-enabled it ; the iPhone tunnel is back on the host mitm-wg).
  2. Switch the LXC to `mitmproxy --mode wireguard` — mitmproxy
     BECOMES the WG endpoint, no DNAT needed, conntrack stays in
     the LXC netns. Requires migrating wg-peers.json to mitm's
     server.conf format + moving UDP 51820 ingress (host nft DNAT
     instead of wg-quick on host).

The LXC infrastructure (privileged template, env-driven launcher,
provisioner) stays shipped as scaffolding for the option-2 migration.
Live state of gk2 host : back on host mitm-wg, iPhone tunnel rx
84 MB / tx 733 MB, growing normally.
2026-06-08 09:41:54 +02:00
f1418b5311 Merge feature/498-lxc-cutover-nm-name : NM name + LXC provisioner refinements (ref #498) 2026-06-08 09:28:57 +02:00
0cf55e6cd0 fix(toolbox): NM connection name + LXC provisioner refinements (ref #498)
Two related improvements + one honest WIP note.

== NM connection id = village3b-r3-<last-octet> ==

The .nmconnection endpoint previously used `client_label` (the visitor
User-Agent) as the NetworkManager connection `id`. Operators ended up
with entries called "Mozilla/5.0 (X11; Linux x86_64; rv:151.0)
Gecko/20100101 Firefox/151.0" in the Network panel. Switched to
"village3b-r3-<last-octet>" : short, stable, distinct per peer (last
octet of the assigned 10.99.1.x).

== lxc-provision refinements ==

Multiple discoveries during the wg LXC migration attempt :

  - lxc-create --template download instead of `debian` (the latter
    requires PRIVILEGED LXC).
  - LXC reads its runtime config from /data/lxc/<name>/config, NOT
    /etc/lxc/<name>.conf. Write a clean, deduped config that
    preserves the auto-generated idmap.
  - Unprivileged LXCs do NOT auto-apply lxc.net.0.ipv4.address (the
    kernel won't let the container set its own address without
    CAP_NET_ADMIN on the netns). Push the IP + default route + DNS
    + systemd-networkd profile via lxc-attach post-start.
  - Install python3 + curl + procps in the container only AFTER the
    network + DNS are up — apt fails earlier.

== Known roadblock — not yet shipped live ==

The wg LXC bind-mount of /etc/secubox/toolbox/ca-wg fails with
"Permission denied" because the dir is 0750 root:secubox-toolbox and
the unprivileged LXC's mapped UID (100000) can't read it. Two
resolutions (documented inline in the conf template) :

  1. chmod 0755 /etc/secubox/toolbox/ca-wg (relaxes CA file
     visibility — OK for the public cert pem, not for the private
     key inside it).
  2. Drop lxc.idmap to switch to privileged mode (matches the
     existing mitmproxy LXC convention).

The host secubox-toolbox-mitm-wg.service stays the active path
until the operator resolves this — no surprise cutover. The iPhone
tunnel is unaffected.
2026-06-08 09:28:47 +02:00
c5f0482edb Merge feature/498-mitm-lxc-hardening : WAF hygiene + onboard page + mitm-wg LXC scaffolding (ref #498) 2026-06-08 09:02:09 +02:00
3c75bbce12 feat: mitm hardening + onboard page + mitm-wg LXC scaffolding (ref #498)
Three independent improvements landed together (small enough that
splitting would have churned the same files twice).

== secubox-waf 1.2.1 : LXC mitmproxy.service hygiene ==

Live diagnosis on gk2 found the LXC mitmproxy WAF process running
1 d 16 h with :
  - 200 MB RSS, 9 threads
  - 934 ESTAB TCP connections persisting
  - 51 % response-error rate on the HAProxy backend
    (eresp = 3.7 M of 7.2 M sessions)
  - one core saturated (87 % of an ARM Cortex-A72)

Phase 6.P had shipped RuntimeMaxSec=21600 (6 h) drop-ins for the
host-side mitm-wg + mitm captive but never reached the LXC service.
Adding it now :
  - RuntimeMaxSec=21600 : clean cycle every 6 h
  - MemoryHigh=400M, MemoryMax=512M : same envelope as host units,
    OOM-kill cleaner than swap thrash under attack traffic
Live restart applied — Until: 12:55:16 UTC.

== secubox-toolbox 2.3.0 : auto-detect /wg/onboard ==

End-user feedback : the current activation flow is fragmented across
"go to kbin.gk2.secubox.in, scan QR, install mobileconfig, trust CA,
…" with per-platform variations. Replaced by a single landing page
that detects the visitor's UA and renders the right one-click flow
first. Sections covered :
  - iOS / iPadOS : WireGuard App Store link + QR + ca.mobileconfig
    + trust toggle reminder + battery-saver caveat.
  - Android : Play Store link + QR + ca.pem + ca.cer + Settings
    path + Samsung/Xiaomi user-CA hint.
  - Linux : .nmconnection + .conf + ca.pem + nmcli reload one-liner
    + update-ca-certificates + Firefox-only note.
  - macOS : Mac App Store link + .conf drag-onto-menu-bar +
    Trousseau system import.
  - Windows : official installer + add-tunnel-from-file + cert
    store import + Firefox cert store caveat.

All artefacts stay at their existing paths ; the onboard page just
composes them. Hand a single URL to anyone — they get the right
flow first.

== secubox-toolbox 2.3.0 : mitm-wg LXC scaffolding ==

secubox-toolbox-lxc-provision now accepts \`transparent\` (default,
existing behaviour) or \`wg\` (new). \`wg\` provisions the
toolbox-mitm-wg LXC at 10.100.0.62 from the existing config +
service templates :

  - Config template rewritten for TRANSPARENT mode on TCP 8081
    (instead of mitmproxy --mode wireguard which would have required
    a server.conf format peer migration). wg-quick stays on the
    host ; only the mitmdump process moves to the LXC.
  - Service template runs the SAME secubox-toolbox-mitm-wg-launch
    script as the host service, bind-mounted in RO — single source
    of truth for the addon chain + ignore_hosts compose logic.
  - RuntimeMaxSec=21600 + memory caps match the WAF LXC's hygiene.

Cutover is OPT-IN — no automatic switchover in postinst. After a
successful provision the script prints :

  1. lxc-attach -n toolbox-mitm-wg -- ss -tln | grep 8081
  2. nft replace rule … dnat ip to 10.100.0.62:8081
  3. systemctl disable --now secubox-toolbox-mitm-wg.service
  4. update /etc/nftables.d/secubox-toolbox-wg.nft for boot-survival.

Operator picks the cutover moment so the live iPhone tunnel session
is never broken mid-flow.
2026-06-08 09:02:00 +02:00
08f3095228 Merge fix/498-crowdsec-health : sudoers + ReadWritePaths for crowdsec health (ref #498) 2026-06-07 09:58:08 +02:00
8d52264eef fix(crowdsec,aggregator): /health bouncers + nft checks under hardened aggregator (ref #498)
WebUI showed firewall errors after Phase 7.D consolidation. Crowdsec
module /health returned `status: degraded` with `bouncers_ok: false,
bouncers_count: 0, nftables_ok: false` even though :

  - `crowdsec.service` was active
  - `crowdsec-firewall-bouncer.service` was active
  - cscli + nft both worked perfectly from a root shell
  - 2 bouncers were properly registered (cs-firewall-bouncer + secubox-waf)
  - inet crowdsec / ip6 crowdsec6 tables were live

Three independent gaps in the new aggregator-mounted module context :

  1. The /health endpoint runs `sudo cscli bouncers list -o json` and
     `sudo nft list tables`, but the aggregator user (`secubox`) had
     no sudoers entry → cscli refused with "permission denied" before
     any actual query.

  2. The aggregator unit had `NoNewPrivileges=true`. Even after
     installing the right sudoers, the subprocess could not gain
     privileges via the setuid sudo binary — systemd's flag blocks
     all privilege escalation including narrowly-scoped sudoers.

  3. The aggregator unit had `ProtectSystem=strict` which makes the
     entire filesystem read-only (incl. /var). When cscli starts, it
     touches perms on /var/lib/crowdsec/data/crowdsec.db. Under
     ProtectSystem=strict that chmod() fails with EROFS and cscli
     exits with rc=1 before printing the bouncer list — even when
     sudo and the sudoers entry are both in order.

Fixes :

  secubox-crowdsec :
    - new sudoers.d/secubox-crowdsec drop-in shipped to
      /etc/sudoers.d/. Two exact-match entries (no wildcards, no
      shell, no flag escapes) :
          secubox ALL=(root) NOPASSWD: /usr/bin/cscli bouncers list -o json
          secubox ALL=(root) NOPASSWD: /usr/sbin/nft list tables
    - debian/rules installs the drop-in to /etc/sudoers.d/ at
      0440 root:root.

  secubox-aggregator :
    - systemd unit : NoNewPrivileges=false so sudo can escalate via
      the sudoers drop-in. The sudoers entries themselves are the
      defense in depth — they cap what the aggregator user can
      escalate to two read-only listings.
    - systemd unit : ReadWritePaths += /var/lib/crowdsec so cscli's
      perm-touch on crowdsec.db succeeds ; += /etc/secubox for the
      toolbox/dpi/etc modules that write config there.

Live verified : status=ok, healthy=true, bouncers_count=2,
nftables_crowdsec=true, nftables_crowdsec6=true.

Bumps :
  secubox-aggregator 0.2.0 → 0.2.1
  secubox-crowdsec 1.0.5 → 1.0.6
2026-06-07 09:57:50 +02:00
be57d52529 Merge chore/498-version-bump-wip : Phase 7.E version bumps + WIP/HISTORY (ref #498) 2026-06-07 09:47:09 +02:00
3433df5eb8 chore: Phase 7.E reboot follow-up — version bumps + WIP + HISTORY (ref #498)
5 packages bumped :
  secubox-toolbox 2.1.0 → 2.2.0  (WG boot survival + R3 detection + NM keyfile + admin rate-limit)
  secubox-aggregator 0.1.0 → 0.2.0  (loader v2 + migration helper v2)
  secubox-waf 1.1.3 → 1.2.0  (/bans/history + Tracked Attackers tile)
  secubox-magicmirror 1.1.0 → 1.1.1  (api/routers/ shipping fix)
  secubox-openclaw 1.0.0 → 1.0.1  (postinst chown for aggregator user)

Plus WIP/HISTORY entries documenting the full reboot-follow-up sprint
(8b0d4840, 22748b29, 613de765, 9ac35005, 7e5c510c, 6812dfb0). Wiki
Phase-7-Roadmap updated in the secubox-deb.wiki repo with a new
Phase 7.E section.
2026-06-07 09:47:01 +02:00
6812dfb0f6 Merge feature/498-nm-keyfile : NetworkManager keyfile export for WG R3 (ref #498) 2026-06-07 08:31:17 +02:00