feat(secubox-toolbox): splash R3 detection — dedicated badge + hides useless R0/R1/R2 switch (ref #496)

User feedback : 'niveau splash incoherent wireguard r3 undetected and
switch r0 r1 r2 available but useless'.

When a request to / comes from 10.99.1.0/24 (WG peer subnet), the splash
handler now :
  - looks up the peer pubkey → wg_hash → R3 mac_hash
  - sets is_wg_r3=True in the template context
  - sets current_level='r3' (was defaulting to None/r1)

Splash template :
  - Top of page : large gradient banner '🌐 Mode R3 — Tunnel WireGuard ACTIF'
    with client IP, links to /report?mh=, PDF download, /wg/r3-install
  - The captive R0/R1/R2/R3 form is HIDDEN entirely for is_wg_r3 (was
    useless : R3 client is by construction max-analysis, can't downgrade
    to captive level)

Validated mode chip now also handles current_level=='r3'.
This commit is contained in:
CyberMind-FR 2026-06-05 14:44:07 +02:00
parent d81e16ac1b
commit 13e48c93c6
2 changed files with 57 additions and 3 deletions

View File

@ -39,11 +39,41 @@ button.lvl-r2{border-color:var(--amber);color:#ffd6a0}
{% if mac_hash and mac_hash != '??' %}<br>Ton appareil (anonymisé) : <b>{{ mac_hash }}</b>{% endif %} {% if mac_hash and mac_hash != '??' %}<br>Ton appareil (anonymisé) : <b>{{ mac_hash }}</b>{% endif %}
</div> </div>
{% if already_validated %} {# Phase 6.I (#496) : R3 WG client detected — show special banner, hide R0/R1/R2 switcher #}
{% if is_wg_r3 %}
<div class="info" style="margin-top:0.5rem;background:linear-gradient(135deg,rgba(158,118,255,0.15),rgba(158,118,255,0.05));border-left:4px solid var(--purple);padding:0.9rem 1rem">
<p style="font-size:1.05rem;font-weight:bold;color:#cbb6ff;margin-bottom:0.4rem">
🌐 <b>Mode R3 — Tunnel WireGuard ACTIF</b>
</p>
<p style="font-size:0.82rem;line-height:1.55;opacity:0.92">
Tu es connecté depuis l'extérieur via le tunnel R3 ({{ client_ip }}).<br>
Le bandeau s'injecte sur les pages HTTPS, Signal/WhatsApp/banques en passthrough.
</p>
<p style="margin-top:0.7rem;display:flex;flex-wrap:wrap;gap:0.5rem">
<a href="/report/me/html?mh={{ mac_hash }}"
style="background:var(--purple);color:#0a0a0f;padding:0.5rem 0.9rem;border-radius:4px;text-decoration:none;font-weight:bold;font-size:0.85rem">
📊 Mon rapport live
</a>
<a href="/report/me?mh={{ mac_hash }}"
style="background:transparent;color:var(--purple);border:1px solid var(--purple);padding:0.5rem 0.9rem;border-radius:4px;text-decoration:none;font-weight:bold;font-size:0.85rem">
⬇ PDF
</a>
<a href="/wg/r3-install"
style="background:transparent;color:#cbb6ff;border:1px solid #555;padding:0.5rem 0.9rem;border-radius:4px;text-decoration:none;font-size:0.82rem">
🔧 Réinstaller R3
</a>
</p>
<p style="font-size:0.7rem;opacity:0.6;margin-top:0.5rem">
💡 R0/R1/R2 sont les niveaux de la cabine captive (WiFi local). En R3 tu es par
construction au niveau maximum d'analyse.
</p>
</div>
{% elif already_validated %}
<div class="info" style="margin-top:0.5rem;background:rgba(0,221,68,0.08);border-left-color:var(--phos)"> <div class="info" style="margin-top:0.5rem;background:rgba(0,221,68,0.08);border-left-color:var(--phos)">
✅ <b>Tu es déjà validé en mode ✅ <b>Tu es déjà validé en mode
{% if current_level == 'r0' %}🌐 R0 (bypass complet) {% if current_level == 'r0' %}🌐 R0 (bypass complet)
{% elif current_level == 'r2' %}🔍 R2 (analyse + bandeau) {% elif current_level == 'r2' %}🔍 R2 (analyse + bandeau)
{% elif current_level == 'r3' %}🌐 R3 (WireGuard tunnel)
{% else %}🛡 R1 (analyse passive){% endif %}</b> {% else %}🛡 R1 (analyse passive){% endif %}</b>
· <a href="/report/me/html" style="color:var(--phos-hot);text-decoration:underline">📊 Voir mon rapport live</a> · <a href="/report/me/html" style="color:var(--phos-hot);text-decoration:underline">📊 Voir mon rapport live</a>
<span style="font-size:0.72rem;opacity:0.7;display:block;margin-top:0.3rem"> <span style="font-size:0.72rem;opacity:0.7;display:block;margin-top:0.3rem">
@ -191,6 +221,8 @@ button.lvl-r2{border-color:var(--amber);color:#ffd6a0}
})(); })();
</script> </script>
{# Phase 6.I : R3 clients skip the captive R0/R1/R2 chooser entirely #}
{% if not is_wg_r3 %}
<p class="choose-label">👇 Choisis ton niveau d'analyse :</p> <p class="choose-label">👇 Choisis ton niveau d'analyse :</p>
<form method="POST" action="/accept" style="display:flex;flex-direction:column;gap:0.6rem"> <form method="POST" action="/accept" style="display:flex;flex-direction:column;gap:0.6rem">
@ -225,6 +257,7 @@ button.lvl-r2{border-color:var(--amber);color:#ffd6a0}
</a> </a>
{% endif %} {% endif %}
</form> </form>
{% endif %} {# /not is_wg_r3 #}
<p class="terms"> <p class="terms">
<b>Conformité :</b> CSPN R2 — consentement explicite par MAC ; LCEN — anonymisation par hash quotidien rotatif. Dispositif défensif (rapport sur TON appareil, jamais sur les tiers). <b>Conformité :</b> CSPN R2 — consentement explicite par MAC ; LCEN — anonymisation par hash quotidien rotatif. Dispositif défensif (rapport sur TON appareil, jamais sur les tiers).

View File

@ -101,6 +101,24 @@ async def splash(request: Request):
salt = _get_salt() salt = _get_salt()
mac_hash = macmod.hash_mac(mac, salt) if mac else None mac_hash = macmod.hash_mac(mac, salt) if mac else None
# Phase 6.I (#496) : if request comes from a WG peer (10.99.1.x), the
# client is by construction R3 — bypass MAC/ARP resolution and use
# the WG peer hash so the splash shows R3 status, masks R0/R1/R2
# switch (useless when already in tunnel), and links to /report?mh=.
client_ip = request.client.host if request.client else None
is_wg_r3 = bool(client_ip and client_ip.startswith("10.99.1."))
if is_wg_r3:
try:
from . import wg as _wg
peers = _wg._load_peers().get("peers", {})
import hashlib as _h
for pk, m in peers.items():
if m.get("ip") == client_ip:
mac_hash = _h.sha256(pk.encode()).hexdigest()[:16]
break
except Exception:
pass
no_cache_headers = { no_cache_headers = {
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
"Pragma": "no-cache", "Pragma": "no-cache",
@ -112,13 +130,16 @@ async def splash(request: Request):
# validated — they had no path back to the install buttons. # validated — they had no path back to the install buttons.
# Phase 6 (#496) : wg_enabled = server.pubkey exists = R3 ready # Phase 6 (#496) : wg_enabled = server.pubkey exists = R3 ready
wg_enabled = Path("/etc/secubox/toolbox/wg/server.pubkey").exists() wg_enabled = Path("/etc/secubox/toolbox/wg/server.pubkey").exists()
current_level = "r3" if is_wg_r3 else (store.get_client_level(mac_hash) if mac_hash else None)
html = _env.get_template("splash.html.j2").render( html = _env.get_template("splash.html.j2").render(
mac_hash=mac_hash or "??", mac_hash=mac_hash or "??",
ssid=cfg.ap.ssid, ssid=cfg.ap.ssid,
r2_enabled=cfg.r2.enabled, r2_enabled=cfg.r2.enabled,
wg_enabled=wg_enabled, wg_enabled=wg_enabled,
already_validated=bool(mac and nft.is_validated(mac)), already_validated=bool(mac and nft.is_validated(mac)) or is_wg_r3,
current_level=store.get_client_level(mac_hash) if mac_hash else None, current_level=current_level,
is_wg_r3=is_wg_r3,
client_ip=client_ip,
) )
return HTMLResponse(html, headers=no_cache_headers) return HTMLResponse(html, headers=no_cache_headers)