- cgcap.ko: cpif CP-frame capture via kprobe/kallsyms, stock-GKI build - ratlock.sh: hold modem to LTE+NR(5G), block 2G/3G downgrade - cgdetect.sh: always-on behavioral cell-site-simulator detection - cgctl.sh + WebUI: block/detect toggles, status + alerts - build-stock-gki.sh: reproducible stock-GKI module build pipeline
136 lines
5.3 KiB
JavaScript
136 lines
5.3 KiB
JavaScript
// CellGuard WebUI — KernelSU-Next ksu.exec() bridge.
|
|
//
|
|
// The WebUI is a viewer/controller only. All work runs in background daemons:
|
|
// ratlock.sh — holds LTE+5G(NR) only (the 2G/3G-block toggle)
|
|
// cgdetect.sh — always-on IMSI-catcher detection -> status + alerts files
|
|
// cgcap.ko — raw CP-frame capture (diag)
|
|
// It talks to them through cgctl.sh, which emits one JSON line per command.
|
|
|
|
(function () {
|
|
const $ = (id) => document.getElementById(id);
|
|
const MOD = "/data/adb/modules/cellguard";
|
|
|
|
const ui = {
|
|
toggle: $("toggle"), dToggle: $("dToggle"), headerState: $("headerState"), dVerdict: $("dVerdict"),
|
|
sRat: $("sRat"), sOp: $("sOp"), sPlmn: $("sPlmn"), sCell: $("sCell"),
|
|
sEarfcn: $("sEarfcn"), sFrames: $("sFrames"), sLock: $("sLock"),
|
|
alerts: $("alerts"), footMsg: $("footMsg"), clrBtn: $("clrBtn"),
|
|
};
|
|
|
|
// KernelSU-Next bridge: ksu.exec(command, optionsJson, callbackFuncName).
|
|
// Results arrive via window[callbackFuncName](errno, stdout, stderr) — the
|
|
// callback is passed BY NAME (a string), not as a function object.
|
|
function exec(cmd) {
|
|
return new Promise((resolve) => {
|
|
if (typeof ksu === "undefined" || !ksu.exec) {
|
|
resolve({ code: -1, stdout: "", stderr: "ksu bridge missing" });
|
|
return;
|
|
}
|
|
const cb = "cg_cb_" + Math.random().toString(36).slice(2);
|
|
let done = false;
|
|
const finish = (code, stdout, stderr) => {
|
|
if (done) return; done = true;
|
|
try { delete window[cb]; } catch (_) {}
|
|
resolve({ code: (code | 0), stdout: stdout || "", stderr: stderr || "" });
|
|
};
|
|
window[cb] = finish;
|
|
try {
|
|
ksu.exec(cmd, "{}", cb);
|
|
} catch (e) {
|
|
// very old bridge with a direct function callback
|
|
try { ksu.exec(cmd, (c, o, er) => finish(c, o, er)); }
|
|
catch (_) { finish(-1, "", String(e)); }
|
|
}
|
|
setTimeout(() => finish(-1, "", "timeout"), 8000);
|
|
});
|
|
}
|
|
|
|
async function ctl(sub) {
|
|
const r = await exec("sh " + MOD + "/cgctl.sh " + sub);
|
|
try { return JSON.parse(r.stdout.trim()); }
|
|
catch (e) { return { ok: false, error: r.stderr || r.stdout || `exit ${r.code}` }; }
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s).replace(/[&<>"']/g, c =>
|
|
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
}
|
|
|
|
function ratBadge(r) {
|
|
const cls = ({
|
|
GSM: "badge-2g", GERAN: "badge-2g", GPRS: "badge-2g", EDGE: "badge-2g",
|
|
UMTS: "badge-3g", WCDMA: "badge-3g", HSPA: "badge-3g", HSPAP: "badge-3g", UTRAN: "badge-3g",
|
|
LTE: "badge-lte", NR: "badge-5g",
|
|
})[r] || "badge-unk";
|
|
return `<span class="badge ${cls}">${esc(r || "?")}</span>`;
|
|
}
|
|
|
|
async function refresh() {
|
|
const s = await ctl("status");
|
|
if (!s || !s.ok) { ui.footMsg.textContent = (s && s.error) || "backend unavailable"; return; }
|
|
|
|
ui.toggle.checked = !!s.block;
|
|
ui.toggle.disabled = false;
|
|
ui.dToggle.checked = !!s.detect;
|
|
ui.dToggle.disabled = false;
|
|
|
|
ui.sRat.innerHTML = ratBadge(s.rat);
|
|
ui.sOp.textContent = (s.operator && s.operator !== "?") ? s.operator : "no service";
|
|
ui.sPlmn.textContent = s.plmn || "?";
|
|
ui.sCell.textContent = `${s.ci || "?"} / ${s.pci || "?"} / ${s.tac || "?"}`;
|
|
ui.sEarfcn.textContent = s.earfcn || "?";
|
|
ui.sFrames.textContent = s.frames || 0;
|
|
ui.sLock.innerHTML = s.lock
|
|
? `<span class="badge badge-5g">LTE + 5G only</span>`
|
|
: `<span class="badge badge-2g">2G/3G allowed</span>`;
|
|
|
|
const bad = s.suspect && s.suspect !== "none";
|
|
ui.dVerdict.innerHTML = bad
|
|
? `<span class="badge badge-2g">${esc(s.suspect)}</span>`
|
|
: `<span class="badge badge-5g">clear</span>`;
|
|
ui.headerState.textContent = bad ? "ALERT" : (s.block ? "armed" : "off");
|
|
ui.headerState.className = "state " + (bad ? "err" : (s.block ? "on" : "off"));
|
|
ui.footMsg.textContent = `RAT ${s.rat || "?"} · ${s.frames || 0} frames`;
|
|
|
|
const a = await ctl("alerts");
|
|
if (a && a.ok) {
|
|
ui.alerts.innerHTML = (a.lines && a.lines.length)
|
|
? a.lines.slice().reverse().map(l =>
|
|
`<div class="cell-row"><div class="left">${esc(l)}</div></div>`).join("")
|
|
: `<div class="empty">No alerts.</div>`;
|
|
}
|
|
}
|
|
|
|
async function onToggle() {
|
|
ui.toggle.disabled = true;
|
|
ui.footMsg.textContent = ui.toggle.checked ? "enabling 2G/3G block…" : "disabling…";
|
|
const r = await ctl("block " + (ui.toggle.checked ? "on" : "off"));
|
|
if (!r || !r.ok) { ui.toggle.checked = !ui.toggle.checked; ui.footMsg.textContent = "failed"; }
|
|
ui.toggle.disabled = false;
|
|
setTimeout(refresh, 800);
|
|
}
|
|
|
|
async function onDetectToggle() {
|
|
ui.dToggle.disabled = true;
|
|
ui.footMsg.textContent = ui.dToggle.checked ? "enabling detection…" : "pausing detection…";
|
|
const r = await ctl("detect " + (ui.dToggle.checked ? "on" : "off"));
|
|
if (!r || !r.ok) { ui.dToggle.checked = !ui.dToggle.checked; ui.footMsg.textContent = "failed"; }
|
|
ui.dToggle.disabled = false;
|
|
setTimeout(refresh, 800);
|
|
}
|
|
|
|
async function onClear() {
|
|
await exec("sh -c ': > /data/adb/cellguard/alerts'");
|
|
refresh();
|
|
}
|
|
|
|
ui.toggle.disabled = true;
|
|
ui.dToggle.disabled = true;
|
|
ui.toggle.addEventListener("change", onToggle);
|
|
ui.dToggle.addEventListener("change", onDetectToggle);
|
|
ui.clrBtn.addEventListener("click", onClear);
|
|
|
|
refresh();
|
|
setInterval(refresh, 3000);
|
|
})();
|