mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 21:38:35 +00:00
Compare commits
2 Commits
55c7d925a6
...
79c6166181
| Author | SHA1 | Date | |
|---|---|---|---|
| 79c6166181 | |||
| 55955867af |
|
|
@ -12,8 +12,8 @@ android {
|
|||
applicationId = "in.secubox.toolbox"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 3
|
||||
versionName = "0.3.0"
|
||||
versionCode = 4
|
||||
versionName = "0.4.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
|
|||
|
|
@ -88,12 +88,23 @@ fun OnboardApp() {
|
|||
busy = false; status = "Borne injoignable — vérifie le réseau."
|
||||
} else {
|
||||
step = Step.RootAuto
|
||||
val onb = RootOnboard(api, ctx.cacheDir)
|
||||
val onb = RootOnboard(api, ctx.cacheDir, ctx.filesDir)
|
||||
val out = withContext(Dispatchers.IO) {
|
||||
onb.runSilent { line -> scope.launch(Dispatchers.Main) { rootLog.add(line) } }
|
||||
}
|
||||
busy = false
|
||||
onTunnel = out.verified
|
||||
// #683 — surface kbin Tor egress status (anonymised exit) if on.
|
||||
rootLog.add(withContext(Dispatchers.IO) {
|
||||
val t = api.torStatus()
|
||||
when {
|
||||
t == null -> "• Statut Tor : indisponible"
|
||||
!t.optBoolean("tor_mode", false) -> "• Mode Tor : inactif"
|
||||
t.optBoolean("running", false) ->
|
||||
"🧅 Mode Tor ACTIF — sortie anonymisée${t.optString("exit_ip", "").let { if (it.isNotBlank() && it != "null") " ($it)" else "" }}"
|
||||
else -> "🧅 Mode Tor activé — tunnel Tor en démarrage…"
|
||||
}
|
||||
})
|
||||
when {
|
||||
out.verified -> step = Step.Done
|
||||
out.wgViaApp -> { step = Step.ImportProfile
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class OnboardService : Service() {
|
|||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
if (!ok) return
|
||||
RootOnboard(api, cacheDir).runSilent { /* headless: no UI log */ }
|
||||
RootOnboard(api, cacheDir, filesDir).runSilent { /* headless: no UI log */ }
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,14 @@ import java.io.File
|
|||
import java.security.MessageDigest
|
||||
import java.security.cert.CertificateFactory
|
||||
|
||||
class RootOnboard(private val api: ToolboxApi, private val cacheDir: File) {
|
||||
class RootOnboard(
|
||||
private val api: ToolboxApi,
|
||||
private val cacheDir: File,
|
||||
// #683: app-internal storage for the STABLE WG identity (survives reboot).
|
||||
// Defaults to cacheDir so older call sites still compile, but real callers
|
||||
// pass filesDir so the identity persists instead of churning each boot.
|
||||
private val filesDir: File = cacheDir,
|
||||
) {
|
||||
|
||||
/** A line appended to the on-screen log during the silent run. */
|
||||
fun interface Logger { fun log(line: String) }
|
||||
|
|
@ -123,8 +130,10 @@ class RootOnboard(private val api: ToolboxApi, private val cacheDir: File) {
|
|||
log.log("• Noyau sans module WireGuard — bascule sur l'app WireGuard")
|
||||
return false
|
||||
}
|
||||
log.log("• Génération du profil WireGuard…")
|
||||
val conf = api.downloadProfile(cacheDir).readText()
|
||||
log.log("• Profil WireGuard (identité stable)…")
|
||||
// #683: reuse the persisted keypair so the device keeps ONE identity
|
||||
// across reboots (no more stats reset to a fresh empty hash each boot).
|
||||
val conf = api.persistentProfile(filesDir).readText()
|
||||
val wg = parse(conf) ?: run { log.log("✗ profil illisible"); return false }
|
||||
val iface = "wg-village3b"
|
||||
val r = RootShell.runScript(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,41 @@ class ToolboxApi(rawHost: String) {
|
|||
fun downloadCa(cacheDir: File): File = download("/wg/ca.crt", "village3b-ca.crt", cacheDir)
|
||||
fun downloadProfile(cacheDir: File): File = download("/wg/profile/new", "village3b-toolbox.conf", cacheDir)
|
||||
|
||||
/**
|
||||
* The device's STABLE WireGuard identity (#683 lost-referrer fix).
|
||||
*
|
||||
* `/wg/profile/new` mints a FRESH keypair on every call. The onboarding
|
||||
* runs on every boot, so calling it each time gave the device a NEW pubkey
|
||||
* → new sha256(pubkey) identity hash → its stats/social history reset to an
|
||||
* empty bucket on every reboot/reconnect. Here we fetch a peer ONCE and
|
||||
* persist the .conf in app-internal `filesDir` (survives reboots, unlike the
|
||||
* evictable cacheDir). Every later call reuses the SAME keypair → SAME
|
||||
* identity → the device keeps one continuous history.
|
||||
*
|
||||
* Survives reboot/reconnect/app-restart. (Reinstall still wipes filesDir;
|
||||
* cross-reinstall persistence would need allowBackup — kept off for CSPN.)
|
||||
*/
|
||||
fun persistentProfile(filesDir: File): File {
|
||||
val stored = File(filesDir, "identity-wg.conf")
|
||||
if (stored.exists() && stored.length() > 0L &&
|
||||
stored.readText().contains("PrivateKey", ignoreCase = true)) {
|
||||
return stored
|
||||
}
|
||||
val fresh = download("/wg/profile/new", "identity-wg.conf.tmp", filesDir)
|
||||
fresh.copyTo(stored, overwrite = true)
|
||||
fresh.delete()
|
||||
return stored
|
||||
}
|
||||
|
||||
/** kbin Tor egress status for the client UI (read-only, kbin-safe). */
|
||||
fun torStatus(): JSONObject? {
|
||||
val c = open("/wg/tor-status")
|
||||
return try {
|
||||
if (c.responseCode !in 200..299) null
|
||||
else JSONObject(c.inputStream.bufferedReader().readText())
|
||||
} catch (_: Exception) { null } finally { c.disconnect() }
|
||||
}
|
||||
|
||||
/** R3 tunnel status. Returns (onTunnel, peerIp?). */
|
||||
fun r3Check(): Pair<Boolean, String?> {
|
||||
val c = open("/wg/r3-check")
|
||||
|
|
|
|||
|
|
@ -65,6 +65,17 @@ async function r3Check(host) {
|
|||
}
|
||||
}
|
||||
|
||||
// #683 — kbin Tor egress status (public, kbin-safe endpoint).
|
||||
async function torStatus(host) {
|
||||
try {
|
||||
const resp = await fetch(`${baseUrl(host)}/wg/tor-status`, { credentials: "omit" });
|
||||
if (!resp.ok) return { tor_mode: false };
|
||||
return await resp.json();
|
||||
} catch (_) {
|
||||
return { tor_mode: false };
|
||||
}
|
||||
}
|
||||
|
||||
// graph: the per-session cartographie JSON. Throws on HTTP error so the
|
||||
// caller can show "token expired — re-pair".
|
||||
async function graph(host, token, since) {
|
||||
|
|
@ -133,6 +144,7 @@ const SbxApi = {
|
|||
setConfig,
|
||||
pair,
|
||||
r3Check,
|
||||
torStatus,
|
||||
graph,
|
||||
wipe,
|
||||
ghost,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "SecuBox ToolBoX — Cartographie sociale",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"description": "Surface the SecuBox R3 toolbox live tracker analysis (cartographie sociale) in your browser: live badge, per-session trackers, mini Round-Eye graph, RGPD wipe + PDF report.",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<body>
|
||||
<header>
|
||||
<span class="logo">👁️ VILLAGE3B</span>
|
||||
<span id="tordot" class="r3 off" title="Mode Tor" style="display:none">🧅</span>
|
||||
<span id="r3dot" class="r3 off" title="État du tunnel R3">R3</span>
|
||||
</header>
|
||||
|
||||
|
|
|
|||
|
|
@ -111,6 +111,21 @@ async function load() {
|
|||
dot.title = r.tunnel ? `Tunnel R3 actif (${r.peer_ip || "?"})` : "Hors tunnel R3";
|
||||
});
|
||||
|
||||
// #683 — Tor egress indicator (only visible when kbin Tor mode is on)
|
||||
api.torStatus(cfg.host).then((t) => {
|
||||
const dot = $("tordot");
|
||||
if (!dot) return;
|
||||
if (t && t.tor_mode) {
|
||||
dot.style.display = "";
|
||||
dot.className = "r3 " + (t.running ? "on" : "off");
|
||||
dot.title = t.running
|
||||
? `Mode Tor actif — sortie anonymisée${t.exit_ip ? " (" + t.exit_ip + ")" : ""}`
|
||||
: "Mode Tor activé — démarrage du tunnel…";
|
||||
} else {
|
||||
dot.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
if (!cfg.token) {
|
||||
$("host").value = cfg.host;
|
||||
show("pair");
|
||||
|
|
|
|||
|
|
@ -1,3 +1,23 @@
|
|||
secubox-toolbox (2.7.6-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* fix(#683): the 🧅 Tor indicator now appears on the ACTUAL injected banner.
|
||||
The live page banner is the client-side stream-inject bundle (bundle.py:
|
||||
"SecuBox · LEVEL · 🛰️ trackers · 🍪 cookies · report ▸ · ✕"), NOT the
|
||||
server-rendered inject_banner chip I'd added earlier. Added `tor_mode` to the
|
||||
decision bundle + a "🧅 Tor" span in the banner render() (loader + inline
|
||||
paths) so a consenting client sees their exit is anonymised.
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Fri, 19 Jun 2026 17:40:00 +0200
|
||||
|
||||
secubox-toolbox (2.7.5-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* feat(#683): public kbin-safe GET /wg/tor-status ({tor_mode, running,
|
||||
bootstrap, exit_ip}) so the clients (webext popup + Android app) can show a
|
||||
🧅 Tor-egress indicator. Mirrors /wg/r3-check (reachable on every vhost incl.
|
||||
public kbin; the /admin/tor/* controls stay admin-gated).
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Fri, 19 Jun 2026 17:00:00 +0200
|
||||
|
||||
secubox-toolbox (2.7.4-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* fix(#683): Tor mode no longer torifies the box's OWN services. kbin/admin
|
||||
|
|
|
|||
|
|
@ -500,6 +500,23 @@ async def change_level(request: Request):
|
|||
status_code=303)
|
||||
|
||||
|
||||
@router.get("/wg/tor-status")
|
||||
async def wg_tor_status() -> dict:
|
||||
"""kbin Tor egress status for the clients (#683). Public + read-only, so it
|
||||
is reachable on the kbin vhost (unlike the admin-gated /admin/tor/*). The
|
||||
webext popup + Android app poll this to show the 🧅 indicator + exit IP."""
|
||||
from .filters import get_filters
|
||||
from . import tor_ctl
|
||||
f = get_filters(force=False)
|
||||
st = tor_ctl.status()
|
||||
return {
|
||||
"tor_mode": bool(f.get("tor_mode", False)),
|
||||
"running": bool(st.get("running", False)),
|
||||
"bootstrap": int(st.get("bootstrap", 0) or 0),
|
||||
"exit_ip": _tor_exit_ip_cached(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/wg/r3-check")
|
||||
async def wg_r3_check(request: Request):
|
||||
"""Phase 7 (#498) — same-origin HTTPS probe for the R3 verification
|
||||
|
|
|
|||
|
|
@ -72,6 +72,15 @@ def _report_url(client_id: str, is_wg: bool) -> str:
|
|||
return REPORT_URL_CAPTIVE
|
||||
|
||||
|
||||
def _tor_mode() -> bool:
|
||||
"""kbin Tor egress on? (#683) Read from filters; fail-safe to off."""
|
||||
try:
|
||||
from .filters import get_filters
|
||||
return bool(get_filters().get("tor_mode", False))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def build_bundle(client_id: str, is_wg: bool = False) -> dict:
|
||||
"""Build the per-client cosmetic decision bundle (pure given inputs + pin file)."""
|
||||
return {
|
||||
|
|
@ -81,6 +90,7 @@ def build_bundle(client_id: str, is_wg: bool = False) -> dict:
|
|||
"pin": _read_pin(),
|
||||
"report_url": _report_url(client_id, is_wg),
|
||||
"tracker_patterns": TRACKER_PATTERNS,
|
||||
"tor_mode": _tor_mode(),
|
||||
"ts": int(time.time()),
|
||||
}
|
||||
|
||||
|
|
@ -157,8 +167,12 @@ _BANNER_CORE = r"""
|
|||
// #662 — 🔓 proof: the engine relaxed this page's CSP to inject this banner.
|
||||
var cspProof = (csp === "1")
|
||||
? "<span title=\"CSP contourné par SecuBox (démonstration)\">🔓</span>" : "";
|
||||
// #683 — 🧅 kbin Tor mode: this session's exit is anonymised via Tor.
|
||||
var tor = b.tor_mode
|
||||
? "<span title=\"Sortie anonymisée via Tor\" style=\"color:#9E76FF;font-weight:bold\">🧅 Tor</span>" : "";
|
||||
bar.innerHTML = "<b style=\"color:#148C66\">SecuBox</b>"
|
||||
+ cspProof
|
||||
+ tor
|
||||
+ "<span>" + esc((b.level || "r1").toUpperCase()) + "</span>"
|
||||
+ "<span>🛰️ " + trk + " trackers</span>"
|
||||
+ "<span>🍪 " + ck + " cookies</span>"
|
||||
|
|
|
|||
|
|
@ -160,5 +160,37 @@ def test_nft_tunnel_failclosed_invariants():
|
|||
assert "meta nfproto ipv6" in text and "drop" in text
|
||||
# only the worker uid is torified (not a blanket rule)
|
||||
assert text.count('meta skuid "secubox-toolbox"') >= 4
|
||||
# loopback + local subnets are exempted (no plumbing breakage)
|
||||
assert "10.99.0.0/16" in text and "10.100.0.0/16" in text
|
||||
# own-services exemption: the reconciler-populated set must exist and be
|
||||
# consulted before the redirect/drop (so the box reaches itself directly)
|
||||
assert "set tor_exempt" in text
|
||||
assert text.count("ip daddr @tor_exempt return") >= 2
|
||||
|
||||
|
||||
def test_bundle_banner_has_tor_indicator(tmp_path, monkeypatch):
|
||||
"""The LIVE injected banner is the stream-inject bundle (bundle.py), not the
|
||||
server-side inject_banner chip. Its render() must show the 🧅 span and the
|
||||
decision bundle must carry tor_mode."""
|
||||
import importlib
|
||||
monkeypatch.setenv("SECUBOX_FILTERS_PATH", str(tmp_path / "filters.json"))
|
||||
import secubox_toolbox.filters as f
|
||||
importlib.reload(f)
|
||||
f.set_filters({"tor_mode": True})
|
||||
import secubox_toolbox.bundle as b
|
||||
importlib.reload(b)
|
||||
assert b.build_bundle("abc", True)["tor_mode"] is True
|
||||
assert b.build_bundle("abc", True) is not None
|
||||
# the banner render() (shared by loader + inline) emits the 🧅 span
|
||||
assert "b.tor_mode" in b.LOADER_JS
|
||||
assert "\U0001F9C5" in b.LOADER_JS # 🧅
|
||||
|
||||
|
||||
def test_reconcile_populates_exempt_and_excludes_automap():
|
||||
"""The reconciler must fill tor_exempt with loopback + own public IP and
|
||||
must NOT exempt the Tor automap range (10.192/10) or transparent proxy breaks."""
|
||||
import pathlib
|
||||
sh = (pathlib.Path(__file__).resolve().parents[1]
|
||||
/ "sbin" / "secubox-toolbox-tor-reconcile").read_text()
|
||||
assert "tor_exempt" in sh and "127.0.0.0/8" in sh
|
||||
assert "api.ipify.org" in sh # own public IP detected direct
|
||||
assert "scope link" in sh # board-local subnets
|
||||
assert "10.19" in sh # explicit automap-range guard
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user