1081 lines
30 KiB
C
1081 lines
30 KiB
C
|
|
// SPDX-License-Identifier: GPL-2.0
|
||
|
|
/*
|
||
|
|
* cellguard.ko — Kernel-enforced cellular security policy
|
||
|
|
*
|
||
|
|
* Blocks 2G (GERAN) and null-cipher connections, forces the modem to
|
||
|
|
* register only on LTE (E-UTRAN) and 5G-SA (NR). A watchdog kthread
|
||
|
|
* polls registration/cipher state and re-asserts the lock within ~1s
|
||
|
|
* of any downgrade attempt.
|
||
|
|
*
|
||
|
|
* /dev/cellguard — character device (write: "on" | "off" |
|
||
|
|
* "release" | "scan"; ioctls below)
|
||
|
|
* /proc/cellguard/status — human-readable state
|
||
|
|
* /proc/cellguard/cells — last scan result (from AT+COPS=?)
|
||
|
|
*
|
||
|
|
* Target: Pixel 10 Pro Fold (rango), Tensor G5, Shannon 5400 modem.
|
||
|
|
* The module opens the tertiary Shannon AT channel (/dev/umts_atc1)
|
||
|
|
* so it does not race RIL (umts_router) or RadioControl (umts_atc0).
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <linux/module.h>
|
||
|
|
#include <linux/kernel.h>
|
||
|
|
#include <linux/init.h>
|
||
|
|
#include <linux/fs.h>
|
||
|
|
#include <linux/cdev.h>
|
||
|
|
#include <linux/device.h>
|
||
|
|
#include <linux/uaccess.h>
|
||
|
|
#include <linux/slab.h>
|
||
|
|
#include <linux/mutex.h>
|
||
|
|
#include <linux/kthread.h>
|
||
|
|
#include <linux/delay.h>
|
||
|
|
#include <linux/ioctl.h>
|
||
|
|
#include <linux/proc_fs.h>
|
||
|
|
#include <linux/seq_file.h>
|
||
|
|
#include <linux/version.h>
|
||
|
|
#include <linux/string.h>
|
||
|
|
#include <linux/ctype.h>
|
||
|
|
#include <linux/namei.h>
|
||
|
|
#include <linux/kprobes.h>
|
||
|
|
|
||
|
|
MODULE_LICENSE("GPL");
|
||
|
|
MODULE_AUTHOR("snake");
|
||
|
|
MODULE_DESCRIPTION("Block 2G / null-cipher cellular; force highest-security RAT");
|
||
|
|
MODULE_VERSION("1.0");
|
||
|
|
|
||
|
|
MODULE_IMPORT_NS(VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver);
|
||
|
|
|
||
|
|
#define DEVICE_NAME "cellguard"
|
||
|
|
#define CLASS_NAME "cellguard"
|
||
|
|
|
||
|
|
#define CMD_BUF_SIZE 512
|
||
|
|
#define RESP_BUF_SIZE 8192
|
||
|
|
#define SCAN_BUF_SIZE 16000
|
||
|
|
|
||
|
|
/* ioctl */
|
||
|
|
#define CG_MAGIC 'C'
|
||
|
|
#define CG_IOC_ENABLE _IO(CG_MAGIC, 1)
|
||
|
|
#define CG_IOC_DISABLE _IO(CG_MAGIC, 2)
|
||
|
|
#define CG_IOC_RELEASE _IO(CG_MAGIC, 3)
|
||
|
|
#define CG_IOC_GET_STATUS _IOR(CG_MAGIC, 4, struct cg_status)
|
||
|
|
#define CG_IOC_SCAN _IOR(CG_MAGIC, 5, struct cg_scan)
|
||
|
|
#define CG_IOC_SET_POLICY _IOW(CG_MAGIC, 6, struct cg_policy)
|
||
|
|
|
||
|
|
struct cg_status {
|
||
|
|
int32_t enabled;
|
||
|
|
int32_t modem_ok;
|
||
|
|
char modem_path[64];
|
||
|
|
char rat[16]; /* "LTE", "NR", "UTRAN", "GERAN", "?" */
|
||
|
|
int32_t rat_code; /* 0=UNK 2=GSM 3=UTRAN 7=LTE 13=NR */
|
||
|
|
int32_t cipher_known;
|
||
|
|
int32_t cipher_null;
|
||
|
|
char operator_name[32];
|
||
|
|
char mcc_mnc[16];
|
||
|
|
int32_t rsrp; /* LTE/NR dBm (0 if unknown) */
|
||
|
|
int32_t tx_pwr;
|
||
|
|
uint64_t downgrades_blocked;
|
||
|
|
uint64_t relocks;
|
||
|
|
uint64_t polls;
|
||
|
|
};
|
||
|
|
struct cg_scan {
|
||
|
|
char data[SCAN_BUF_SIZE];
|
||
|
|
uint32_t data_len;
|
||
|
|
int32_t status; /* 0=OK, negative=error */
|
||
|
|
};
|
||
|
|
|
||
|
|
struct cg_policy {
|
||
|
|
int32_t block_2g; /* 1=block GERAN (default) */
|
||
|
|
int32_t block_3g; /* 1=block UTRAN (default) */
|
||
|
|
int32_t block_null_cipher; /* 1=block A5/0 (default) */
|
||
|
|
int32_t poll_interval_ms; /* default 1500 */
|
||
|
|
};
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Per-carrier LTE/NR band profile. Band numbers terminate with 0.
|
||
|
|
* Masks are built from these at push time.
|
||
|
|
*/
|
||
|
|
struct carrier_profile {
|
||
|
|
char name[16];
|
||
|
|
int lte_bands[32];
|
||
|
|
int nr_bands[32];
|
||
|
|
};
|
||
|
|
|
||
|
|
/* US carriers. Band sets as of 2026 deployment data. */
|
||
|
|
static const struct carrier_profile carrier_profiles[] = {
|
||
|
|
{ "none", {0}, {0} },
|
||
|
|
{ "tmobile", {2,4,5,12,25,41,66,71,0}, {25,41,66,71,258,260,261,0} },
|
||
|
|
{ "verizon", {2,4,5,13,46,48,66,0}, {2,5,66,77,260,261,0} },
|
||
|
|
{ "att", {2,4,5,12,14,17,29,30,66,0}, {2,5,14,30,66,77,260,261,0} },
|
||
|
|
{ "uscellular", {2,4,5,12,25,26,66,71,0}, {66,71,77,0} },
|
||
|
|
{ "dish", {29,66,70,71,0}, {29,66,70,71,0} },
|
||
|
|
{ "cricket", {2,4,5,12,14,17,29,30,66,0}, {2,5,14,30,66,77,260,261,0} },
|
||
|
|
{ "metro", {2,4,5,12,25,41,66,71,0}, {25,41,66,71,258,260,261,0} },
|
||
|
|
{ "googlefi", {2,4,5,12,25,41,66,71,0}, {25,41,66,71,258,260,261,0} },
|
||
|
|
{ "generic_us", {2,4,5,12,13,14,17,25,26,29,30,41,46,48,66,71,0},
|
||
|
|
{2,5,14,25,30,41,66,70,71,77,78,260,261,0} },
|
||
|
|
};
|
||
|
|
#define N_CARRIERS ARRAY_SIZE(carrier_profiles)
|
||
|
|
|
||
|
|
/* Shannon AT channels, in order of preference.
|
||
|
|
* umts_atc1 is tertiary — not claimed by RIL or RadioControl.
|
||
|
|
*/
|
||
|
|
static const char *modem_paths[] = {
|
||
|
|
"/dev/umts_atc1",
|
||
|
|
"/dev/nr_atc0",
|
||
|
|
"/dev/umts_atc0",
|
||
|
|
"/dev/umts_router",
|
||
|
|
"/dev/umts_router0",
|
||
|
|
NULL
|
||
|
|
};
|
||
|
|
|
||
|
|
static int major;
|
||
|
|
static struct class *cg_class;
|
||
|
|
static struct cdev cg_cdev;
|
||
|
|
static struct device *cg_dev;
|
||
|
|
static struct file *modem_filp;
|
||
|
|
static char modem_path_used[64];
|
||
|
|
static DEFINE_MUTEX(at_mutex);
|
||
|
|
|
||
|
|
static struct task_struct *watchdog_thread;
|
||
|
|
static struct cg_policy policy = {
|
||
|
|
.block_2g = 1,
|
||
|
|
.block_3g = 1,
|
||
|
|
.block_null_cipher = 1,
|
||
|
|
.poll_interval_ms = 1500,
|
||
|
|
};
|
||
|
|
static bool guard_enabled;
|
||
|
|
|
||
|
|
/* Current carrier + band-lock state */
|
||
|
|
static int current_carrier; /* index into carrier_profiles; 0=none */
|
||
|
|
static int bands_locked; /* 1 if last push returned OK */
|
||
|
|
static char bands_push_method[32]; /* which AT form the modem accepted */
|
||
|
|
|
||
|
|
/* Cached state (updated by watchdog) */
|
||
|
|
static char cur_rat[16] = "?";
|
||
|
|
static int cur_rat_code;
|
||
|
|
static int cur_cipher_known;
|
||
|
|
static int cur_cipher_null;
|
||
|
|
static char cur_operator[32] = "";
|
||
|
|
static char cur_mcc_mnc[16] = "";
|
||
|
|
static int cur_rsrp;
|
||
|
|
static int cur_tx_pwr = -140;
|
||
|
|
static int cur_pwr_known;
|
||
|
|
static uint64_t stat_downgrades;
|
||
|
|
static uint64_t stat_relocks;
|
||
|
|
static uint64_t stat_polls;
|
||
|
|
|
||
|
|
/* Last AT+COPS=? scan result (raw) */
|
||
|
|
static char scan_buf[SCAN_BUF_SIZE];
|
||
|
|
static int scan_len;
|
||
|
|
static DEFINE_MUTEX(scan_mutex);
|
||
|
|
|
||
|
|
/* Global AT response buffer to avoid stack frame overflow */
|
||
|
|
static char *at_resp_buf;
|
||
|
|
#define AT_RESP_SIZE RESP_BUF_SIZE
|
||
|
|
|
||
|
|
/* ---------- GKI protected-symbol resolution ----------
|
||
|
|
*
|
||
|
|
* Stock GKI (CONFIG_MODULE_SIG_PROTECT) refuses to load an *unsigned*
|
||
|
|
* module that link-imports a protected core symbol. kernel_read() and
|
||
|
|
* kernel_write() are both protected, so importing them yields
|
||
|
|
* "Protected symbol: kernel_write (err -13)" at insmod time.
|
||
|
|
*
|
||
|
|
* Instead we resolve them at load time via kallsyms. The only link-time
|
||
|
|
* dependency this adds is the kprobe API (register/unregister_kprobe),
|
||
|
|
* which is part of the stable vendor KMI. Net effect: the module loads
|
||
|
|
* unsigned on any stock GKI build, and is portable across devices/builds
|
||
|
|
* because nothing protected is bound at link time.
|
||
|
|
*/
|
||
|
|
typedef ssize_t (*cg_kernel_read_t)(struct file *, void *, size_t, loff_t *);
|
||
|
|
typedef ssize_t (*cg_kernel_write_t)(struct file *, const void *, size_t,
|
||
|
|
loff_t *);
|
||
|
|
static cg_kernel_read_t cg_kernel_read;
|
||
|
|
static cg_kernel_write_t cg_kernel_write;
|
||
|
|
|
||
|
|
/* Obtain kallsyms_lookup_name() (itself unexported) via a one-shot kprobe. */
|
||
|
|
static unsigned long cg_lookup_name(const char *name)
|
||
|
|
{
|
||
|
|
static unsigned long (*kln)(const char *name);
|
||
|
|
|
||
|
|
if (!kln) {
|
||
|
|
struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };
|
||
|
|
int ret = register_kprobe(&kp);
|
||
|
|
|
||
|
|
if (ret < 0) {
|
||
|
|
pr_err("cellguard: kprobe bootstrap failed (%d)\n", ret);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
kln = (void *)kp.addr;
|
||
|
|
unregister_kprobe(&kp);
|
||
|
|
}
|
||
|
|
return kln ? kln(name) : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int cg_resolve_protected(void)
|
||
|
|
{
|
||
|
|
cg_kernel_read = (cg_kernel_read_t) cg_lookup_name("kernel_read");
|
||
|
|
cg_kernel_write = (cg_kernel_write_t)cg_lookup_name("kernel_write");
|
||
|
|
if (!cg_kernel_read || !cg_kernel_write) {
|
||
|
|
pr_err("cellguard: failed to resolve kernel_read/kernel_write\n");
|
||
|
|
return -ENOENT;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- modem I/O ---------- */
|
||
|
|
|
||
|
|
static struct file *open_modem(void)
|
||
|
|
{
|
||
|
|
struct file *f;
|
||
|
|
struct path path;
|
||
|
|
int i, ret;
|
||
|
|
|
||
|
|
for (i = 0; modem_paths[i]; i++) {
|
||
|
|
ret = kern_path(modem_paths[i], LOOKUP_FOLLOW, &path);
|
||
|
|
if (ret)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
f = dentry_open(&path, O_RDWR | O_NONBLOCK, current_cred());
|
||
|
|
path_put(&path);
|
||
|
|
|
||
|
|
if (!IS_ERR(f)) {
|
||
|
|
strscpy(modem_path_used, modem_paths[i],
|
||
|
|
sizeof(modem_path_used));
|
||
|
|
pr_info("cellguard: using AT channel %s\n",
|
||
|
|
modem_paths[i]);
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ERR_PTR(-ENODEV);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Send one AT command, collect response up to OK/ERROR or timeout.
|
||
|
|
* Must be called with at_mutex held.
|
||
|
|
*/
|
||
|
|
static int at_cmd(const char *cmd, char *resp, int resp_size, int timeout_ms)
|
||
|
|
{
|
||
|
|
char fullcmd[CMD_BUF_SIZE];
|
||
|
|
loff_t pos = 0;
|
||
|
|
ssize_t w, r;
|
||
|
|
int elapsed = 0, total = 0;
|
||
|
|
int clen;
|
||
|
|
|
||
|
|
if (!modem_filp || IS_ERR(modem_filp))
|
||
|
|
return -ENODEV;
|
||
|
|
|
||
|
|
clen = scnprintf(fullcmd, sizeof(fullcmd), "%s\r\n", cmd);
|
||
|
|
w = cg_kernel_write(modem_filp, fullcmd, clen, &pos);
|
||
|
|
if (w < 0)
|
||
|
|
return (int)w;
|
||
|
|
|
||
|
|
memset(resp, 0, resp_size);
|
||
|
|
pos = 0;
|
||
|
|
|
||
|
|
while (elapsed < timeout_ms && total < resp_size - 1) {
|
||
|
|
r = cg_kernel_read(modem_filp, resp + total,
|
||
|
|
resp_size - 1 - total, &pos);
|
||
|
|
if (r > 0) {
|
||
|
|
total += r;
|
||
|
|
resp[total] = '\0';
|
||
|
|
if (strstr(resp, "\r\nOK\r\n") ||
|
||
|
|
strstr(resp, "\r\nERROR\r\n") ||
|
||
|
|
strstr(resp, "+CME ERROR:") ||
|
||
|
|
strstr(resp, "+CMS ERROR:"))
|
||
|
|
break;
|
||
|
|
} else {
|
||
|
|
msleep(50);
|
||
|
|
elapsed += 50;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (total == 0)
|
||
|
|
return -ETIMEDOUT;
|
||
|
|
return total;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- band mask helpers ---------- */
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Build a hex bitmap from a zero-terminated band list. Band N maps to
|
||
|
|
* bit (N-1). Output is up to 64 hex digits (256 bits), leading zeros
|
||
|
|
* trimmed. Returns the length written.
|
||
|
|
*/
|
||
|
|
static int hex_mask_from_bands(const int *bands, char *out, int out_sz)
|
||
|
|
{
|
||
|
|
uint64_t mask[4] = { 0, 0, 0, 0 };
|
||
|
|
int i, top, written;
|
||
|
|
|
||
|
|
for (i = 0; bands[i]; i++) {
|
||
|
|
int b = bands[i] - 1;
|
||
|
|
if (b < 0 || b >= 256)
|
||
|
|
continue;
|
||
|
|
mask[b >> 6] |= 1ULL << (b & 63);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Find highest non-zero word to trim leading zeros */
|
||
|
|
for (top = 3; top > 0 && mask[top] == 0; top--) ;
|
||
|
|
|
||
|
|
if (top == 0)
|
||
|
|
written = scnprintf(out, out_sz, "%llx", mask[0]);
|
||
|
|
else if (top == 1)
|
||
|
|
written = scnprintf(out, out_sz, "%llx%016llx",
|
||
|
|
mask[1], mask[0]);
|
||
|
|
else if (top == 2)
|
||
|
|
written = scnprintf(out, out_sz, "%llx%016llx%016llx",
|
||
|
|
mask[2], mask[1], mask[0]);
|
||
|
|
else
|
||
|
|
written = scnprintf(out, out_sz, "%llx%016llx%016llx%016llx",
|
||
|
|
mask[3], mask[2], mask[1], mask[0]);
|
||
|
|
return written;
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Build a comma-separated list of band numbers (for AT forms that
|
||
|
|
* accept a list instead of a bitmap).
|
||
|
|
*/
|
||
|
|
static int list_from_bands(const int *bands, char *out, int out_sz)
|
||
|
|
{
|
||
|
|
int i, n = 0;
|
||
|
|
out[0] = '\0';
|
||
|
|
for (i = 0; bands[i]; i++) {
|
||
|
|
int w = scnprintf(out + n, out_sz - n,
|
||
|
|
i == 0 ? "%d" : ",%d", bands[i]);
|
||
|
|
if (w <= 0)
|
||
|
|
break;
|
||
|
|
n += w;
|
||
|
|
}
|
||
|
|
return n;
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Try several known Shannon band-lock AT command forms in order. Stops
|
||
|
|
* at the first that returns OK. Records which form worked so status
|
||
|
|
* output can show it.
|
||
|
|
*
|
||
|
|
* Returns 1 if any form was accepted, 0 otherwise. Called with
|
||
|
|
* at_mutex held.
|
||
|
|
*/
|
||
|
|
static int try_band_lock(const char *rat_tag, int rat_id,
|
||
|
|
const int *bands)
|
||
|
|
{
|
||
|
|
char mask_hex[80];
|
||
|
|
char mask_list[256];
|
||
|
|
char cmd[320];
|
||
|
|
int i, n;
|
||
|
|
|
||
|
|
/* Empty band list -> nothing to do. */
|
||
|
|
if (!bands[0])
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
hex_mask_from_bands(bands, mask_hex, sizeof(mask_hex));
|
||
|
|
list_from_bands(bands, mask_list, sizeof(mask_list));
|
||
|
|
|
||
|
|
struct variant {
|
||
|
|
const char *fmt;
|
||
|
|
bool use_list;
|
||
|
|
bool use_property;
|
||
|
|
};
|
||
|
|
static const struct variant lte_variants[] = {
|
||
|
|
{ "AT+GOOGSETPROPERTY=\"NASU.LTE Band Priority\",\"%s\"", true, true },
|
||
|
|
{ "AT+LBANDSEL=0x%s", false, false },
|
||
|
|
{ "AT+LBANDSEL=%s", false, false },
|
||
|
|
{ "AT+BANDIND=%s", false, false },
|
||
|
|
{ "AT$BANDSEL=7,%s", false, false },
|
||
|
|
{ "AT$BANDLOCK=7,%s", false, false },
|
||
|
|
{ "AT+BANDSEL=7,%s", true, false },
|
||
|
|
{ "AT+QCFG=\"band\",0,%s,0,1", false, false },
|
||
|
|
{ NULL, false, false }
|
||
|
|
};
|
||
|
|
static const struct variant nr_variants[] = {
|
||
|
|
{ "AT+NRBANDSEL=0x%s", false, false },
|
||
|
|
{ "AT+NRBANDSEL=%s", false, false },
|
||
|
|
{ "AT+NRBANDIND=%s", false, false },
|
||
|
|
{ "AT$BANDSEL=12,%s", false, false },
|
||
|
|
{ "AT$BANDLOCK=12,%s", false, false },
|
||
|
|
{ "AT+BANDSEL=12,%s", true, false },
|
||
|
|
{ "AT+QCFG=\"nr5g_band\",%s", false, false },
|
||
|
|
{ NULL, false, false }
|
||
|
|
};
|
||
|
|
const struct variant *variants =
|
||
|
|
(rat_id == 12) ? nr_variants : lte_variants;
|
||
|
|
|
||
|
|
for (i = 0; variants[i].fmt; i++) {
|
||
|
|
const char *arg = variants[i].use_list ? mask_list : mask_hex;
|
||
|
|
scnprintf(cmd, sizeof(cmd), variants[i].fmt, arg);
|
||
|
|
|
||
|
|
n = at_cmd(cmd, at_resp_buf, AT_RESP_SIZE, 3000);
|
||
|
|
if (n > 0 && (strstr(at_resp_buf, "\r\nOK\r\n") ||
|
||
|
|
strstr(at_resp_buf, "\nOK\n"))) {
|
||
|
|
pr_info("cellguard: %s band-lock via '%s' accepted\n",
|
||
|
|
rat_tag, variants[i].fmt);
|
||
|
|
snprintf(bands_push_method, sizeof(bands_push_method),
|
||
|
|
"%s:%s", rat_tag, variants[i].fmt);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void push_band_lock(void)
|
||
|
|
{
|
||
|
|
const struct carrier_profile *p;
|
||
|
|
int lte_ok, nr_ok;
|
||
|
|
|
||
|
|
bands_locked = 0;
|
||
|
|
bands_push_method[0] = '\0';
|
||
|
|
|
||
|
|
if (current_carrier <= 0 || current_carrier >= N_CARRIERS)
|
||
|
|
return;
|
||
|
|
if (!modem_filp)
|
||
|
|
return;
|
||
|
|
|
||
|
|
p = &carrier_profiles[current_carrier];
|
||
|
|
|
||
|
|
/* at_mutex is held by the caller (push_lock_policy). */
|
||
|
|
lte_ok = try_band_lock("LTE", 7, p->lte_bands);
|
||
|
|
nr_ok = try_band_lock("NR", 12, p->nr_bands);
|
||
|
|
|
||
|
|
if (lte_ok || nr_ok)
|
||
|
|
bands_locked = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- policy push ---------- */
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Lock modem to LTE+NR, enable ciphering-indication URCs, and keep
|
||
|
|
* the 2G/3G RATs disabled. The commands below are 3GPP-standard
|
||
|
|
* where possible, with Shannon-specific fallbacks.
|
||
|
|
*
|
||
|
|
* AT+WS46=28 — select E-UTRAN + NR (TS 27.007)
|
||
|
|
* AT+CEMODE=2 — LTE/NR PS+CS only (Samsung NVRAM hint)
|
||
|
|
* AT+CGDCONT... — not touched
|
||
|
|
* AT$CIPHERINGIND=1 — enable URC on cipher-algorithm change
|
||
|
|
* AT+CSCS="GSM" — ensure ASCII responses
|
||
|
|
*
|
||
|
|
* We do NOT modify SIM/network search lists; user can still roam.
|
||
|
|
*/
|
||
|
|
static void push_lock_policy(void)
|
||
|
|
{
|
||
|
|
int ws46;
|
||
|
|
|
||
|
|
if (!modem_filp)
|
||
|
|
return;
|
||
|
|
|
||
|
|
/* Compose WS46 value from policy:
|
||
|
|
* 25 = UTRAN+E-UTRAN+NR (3G allowed)
|
||
|
|
* 28 = E-UTRAN+NR only (hardened, default)
|
||
|
|
* 29 = GERAN+UTRAN+E-UTRAN (2G allowed)
|
||
|
|
* 30 = E-UTRAN only
|
||
|
|
* 31 = NR only
|
||
|
|
*/
|
||
|
|
if (policy.block_2g && policy.block_3g)
|
||
|
|
ws46 = 28;
|
||
|
|
else if (policy.block_2g && !policy.block_3g)
|
||
|
|
ws46 = 25;
|
||
|
|
else
|
||
|
|
ws46 = 22; /* default (all) */
|
||
|
|
|
||
|
|
mutex_lock(&at_mutex);
|
||
|
|
|
||
|
|
at_cmd("ATE0", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
at_cmd("AT+CMEE=1", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
at_cmd("AT+CSCS=\"GSM\"", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
|
||
|
|
{
|
||
|
|
char cmd[32];
|
||
|
|
scnprintf(cmd, sizeof(cmd), "AT+WS46=%d", ws46);
|
||
|
|
at_cmd(cmd, at_resp_buf, AT_RESP_SIZE, 3000);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Shannon-specific: ciphering indication URC */
|
||
|
|
at_cmd("AT$CIPHERINGIND=1", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
at_cmd("AT+CIPHERINGIND=1", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
|
||
|
|
/* Google/Pixel specific: Null cipher kill switch */
|
||
|
|
if (policy.block_null_cipher)
|
||
|
|
at_cmd("AT+GOOGSETPROPERTY=\"NASU.NULL.CIPHER.INTEGRITY.CTRL\",\"0\"", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
else
|
||
|
|
at_cmd("AT+GOOGSETPROPERTY=\"NASU.NULL.CIPHER.INTEGRITY.CTRL\",\"1\"", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
|
||
|
|
/* Enable registration URCs so we see RAT changes fast */
|
||
|
|
at_cmd("AT+CREG=2", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
at_cmd("AT+CGREG=2", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
at_cmd("AT+CEREG=2", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
at_cmd("AT+C5GREG=2", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
|
||
|
|
/* Apply carrier band lock BEFORE the CFUN cycle so the radio
|
||
|
|
* comes back up with the restricted band set active.
|
||
|
|
*/
|
||
|
|
push_band_lock();
|
||
|
|
|
||
|
|
/* Kick the radio so the new WS46 takes effect */
|
||
|
|
at_cmd("AT+CFUN=4", at_resp_buf, AT_RESP_SIZE, 3000);
|
||
|
|
msleep(300);
|
||
|
|
at_cmd("AT+CFUN=1", at_resp_buf, AT_RESP_SIZE, 5000);
|
||
|
|
|
||
|
|
stat_relocks++;
|
||
|
|
mutex_unlock(&at_mutex);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Release the lock: reselect all RATs so normal user preference
|
||
|
|
* resumes. Called on disable/unload.
|
||
|
|
*/
|
||
|
|
static void release_lock_policy(void)
|
||
|
|
{
|
||
|
|
if (!modem_filp)
|
||
|
|
return;
|
||
|
|
|
||
|
|
mutex_lock(&at_mutex);
|
||
|
|
at_cmd("AT+WS46=22", at_resp_buf, AT_RESP_SIZE, 3000);
|
||
|
|
at_cmd("AT+CFUN=4", at_resp_buf, AT_RESP_SIZE, 3000);
|
||
|
|
msleep(300);
|
||
|
|
at_cmd("AT+CFUN=1", at_resp_buf, AT_RESP_SIZE, 5000);
|
||
|
|
mutex_unlock(&at_mutex);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- state polling ---------- */
|
||
|
|
|
||
|
|
static int parse_int_field(const char *s, int field, int *out)
|
||
|
|
{
|
||
|
|
int i = 0;
|
||
|
|
const char *p = s;
|
||
|
|
|
||
|
|
while (*p && i < field) {
|
||
|
|
if (*p++ == ',')
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
while (*p == ' ' || *p == '"')
|
||
|
|
p++;
|
||
|
|
if (!isdigit(*p) && *p != '-')
|
||
|
|
return -1;
|
||
|
|
*out = simple_strtol(p, NULL, 10);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* +CREG AcT codes (TS 27.007):
|
||
|
|
* 0 GSM (2G)
|
||
|
|
* 1 GSM Compact (2G)
|
||
|
|
* 2 UTRAN (3G)
|
||
|
|
* 3 EDGE (2G)
|
||
|
|
* 4 UTRAN/HSDPA (3G)
|
||
|
|
* 5 UTRAN/HSUPA (3G)
|
||
|
|
* 6 UTRAN/HSPA+ (3G)
|
||
|
|
* 7 E-UTRAN (4G)
|
||
|
|
* 8 EC-GSM-IoT (2G IoT)
|
||
|
|
* 9 E-UTRAN NB-S1 (4G IoT)
|
||
|
|
* 10 E-UTRAN (4G)
|
||
|
|
* 11 NR connected to 5GC (5G-SA)
|
||
|
|
* 13 NR (5G-SA)
|
||
|
|
*/
|
||
|
|
static void classify_rat(int act, char *name, int name_sz, int *is_2g, int *is_3g)
|
||
|
|
{
|
||
|
|
*is_2g = 0;
|
||
|
|
*is_3g = 0;
|
||
|
|
switch (act) {
|
||
|
|
case 0: case 1: case 3: case 8:
|
||
|
|
strscpy(name, "GERAN", name_sz); *is_2g = 1; break;
|
||
|
|
case 2: case 4: case 5: case 6:
|
||
|
|
strscpy(name, "UTRAN", name_sz); *is_3g = 1; break;
|
||
|
|
case 7: case 9: case 10:
|
||
|
|
strscpy(name, "LTE", name_sz); break;
|
||
|
|
case 11: case 13:
|
||
|
|
strscpy(name, "NR", name_sz); break;
|
||
|
|
default:
|
||
|
|
strscpy(name, "?", name_sz); break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* One poll cycle. Returns 1 if a downgrade was detected and relock
|
||
|
|
* should be pushed, 0 otherwise.
|
||
|
|
*/
|
||
|
|
static int poll_once(void)
|
||
|
|
{
|
||
|
|
char *p;
|
||
|
|
int act = -1, stat = -1;
|
||
|
|
int is_2g = 0, is_3g = 0;
|
||
|
|
int downgraded = 0;
|
||
|
|
int n;
|
||
|
|
|
||
|
|
if (!modem_filp)
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
mutex_lock(&at_mutex);
|
||
|
|
stat_polls++;
|
||
|
|
|
||
|
|
/* Prefer +C5GREG, fall back to +CEREG, then +CREG */
|
||
|
|
n = at_cmd("AT+C5GREG?", at_resp_buf, AT_RESP_SIZE, 1500);
|
||
|
|
if (n > 0 && (p = strstr(at_resp_buf, "+C5GREG:"))) {
|
||
|
|
if (parse_int_field(p + 8, 0, &stat) == 0 &&
|
||
|
|
parse_int_field(p + 8, 3, &act) == 0 &&
|
||
|
|
(stat == 1 || stat == 5)) {
|
||
|
|
/* got NR registration */
|
||
|
|
} else {
|
||
|
|
act = -1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (act < 0) {
|
||
|
|
n = at_cmd("AT+CEREG?", at_resp_buf, AT_RESP_SIZE, 1500);
|
||
|
|
if (n > 0 && (p = strstr(at_resp_buf, "+CEREG:"))) {
|
||
|
|
if (parse_int_field(p + 7, 0, &stat) == 0 &&
|
||
|
|
parse_int_field(p + 7, 3, &act) == 0 &&
|
||
|
|
(stat == 1 || stat == 5)) {
|
||
|
|
/* registered */
|
||
|
|
} else {
|
||
|
|
act = -1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (act < 0) {
|
||
|
|
n = at_cmd("AT+CREG?", at_resp_buf, AT_RESP_SIZE, 1500);
|
||
|
|
if (n > 0 && (p = strstr(at_resp_buf, "+CREG:"))) {
|
||
|
|
if (parse_int_field(p + 6, 0, &stat) == 0 &&
|
||
|
|
parse_int_field(p + 6, 3, &act) == 0 &&
|
||
|
|
(stat == 1 || stat == 5)) {
|
||
|
|
/* registered on legacy */
|
||
|
|
} else {
|
||
|
|
act = -1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (act >= 0) {
|
||
|
|
classify_rat(act, cur_rat, sizeof(cur_rat), &is_2g, &is_3g);
|
||
|
|
cur_rat_code = act;
|
||
|
|
if ((policy.block_2g && is_2g) ||
|
||
|
|
(policy.block_3g && is_3g))
|
||
|
|
downgraded = 1;
|
||
|
|
} else {
|
||
|
|
strscpy(cur_rat, "?", sizeof(cur_rat));
|
||
|
|
cur_rat_code = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Operator name + MCC/MNC */
|
||
|
|
n = at_cmd("AT+COPS?", at_resp_buf, AT_RESP_SIZE, 2000);
|
||
|
|
if (n > 0 && (p = strstr(at_resp_buf, "+COPS:"))) {
|
||
|
|
char *q = strchr(p, '"');
|
||
|
|
if (q) {
|
||
|
|
char *e;
|
||
|
|
q++;
|
||
|
|
e = strchr(q, '"');
|
||
|
|
if (e) {
|
||
|
|
int len = min_t(int, (int)(e - q),
|
||
|
|
(int)sizeof(cur_operator) - 1);
|
||
|
|
memcpy(cur_operator, q, len);
|
||
|
|
cur_operator[len] = '\0';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
n = at_cmd("AT+COPS=3,2", at_resp_buf, AT_RESP_SIZE, 1500);
|
||
|
|
(void)n;
|
||
|
|
n = at_cmd("AT+COPS?", at_resp_buf, AT_RESP_SIZE, 1500);
|
||
|
|
if (n > 0 && (p = strstr(at_resp_buf, "+COPS:"))) {
|
||
|
|
char *q = strchr(p, '"');
|
||
|
|
if (q) {
|
||
|
|
char *e;
|
||
|
|
q++;
|
||
|
|
e = strchr(q, '"');
|
||
|
|
if (e) {
|
||
|
|
int len = min_t(int, (int)(e - q),
|
||
|
|
(int)sizeof(cur_mcc_mnc) - 1);
|
||
|
|
memcpy(cur_mcc_mnc, q, len);
|
||
|
|
cur_mcc_mnc[len] = '\0';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/* Restore long alpha format for next poll */
|
||
|
|
at_cmd("AT+COPS=3,0", at_resp_buf, AT_RESP_SIZE, 1000);
|
||
|
|
|
||
|
|
/* Signal (LTE/NR): +CESQ rsrp is field 5 (0..97), dBm = -140 + v */
|
||
|
|
n = at_cmd("AT+CESQ", at_resp_buf, AT_RESP_SIZE, 1500);
|
||
|
|
if (n > 0 && (p = strstr(at_resp_buf, "+CESQ:"))) {
|
||
|
|
int rsrp_raw;
|
||
|
|
if (parse_int_field(p + 6, 5, &rsrp_raw) == 0 && rsrp_raw < 97)
|
||
|
|
cur_rsrp = -140 + rsrp_raw;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Google specific: TX power */
|
||
|
|
cur_pwr_known = 0;
|
||
|
|
cur_tx_pwr = -140;
|
||
|
|
n = at_cmd("AT+GOOGGETTXPWR", at_resp_buf, AT_RESP_SIZE, 1500);
|
||
|
|
if (n > 0 && (p = strstr(at_resp_buf, "+GOOGGETTXPWR:"))) {
|
||
|
|
/* Format: +GOOGGETTXPWR: " -12.5" or similar */
|
||
|
|
char *q = strchr(p, '"');
|
||
|
|
if (q) {
|
||
|
|
cur_pwr_known = 1;
|
||
|
|
cur_tx_pwr = (int)simple_strtol(q + 1, NULL, 10);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Cipher check — Shannon $CIPHERINGIND or 3GPP (rarely supported).
|
||
|
|
* We mark "unknown" if the modem doesn't expose it; downgrade to
|
||
|
|
* 2G is the practical null-cipher indicator we care about.
|
||
|
|
*/
|
||
|
|
cur_cipher_known = 0;
|
||
|
|
cur_cipher_null = 0;
|
||
|
|
n = at_cmd("AT$CIPHERINGIND?", at_resp_buf, AT_RESP_SIZE, 1500);
|
||
|
|
if (n > 0 && (p = strstr(at_resp_buf, "$CIPHERINGIND:"))) {
|
||
|
|
int ind;
|
||
|
|
if (parse_int_field(p + 14, 0, &ind) == 0) {
|
||
|
|
cur_cipher_known = 1;
|
||
|
|
/* ind==0 => no ciphering / weak cipher */
|
||
|
|
if (ind == 0) {
|
||
|
|
cur_cipher_null = 1;
|
||
|
|
if (policy.block_null_cipher)
|
||
|
|
downgraded = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (downgraded)
|
||
|
|
stat_downgrades++;
|
||
|
|
|
||
|
|
mutex_unlock(&at_mutex);
|
||
|
|
return downgraded;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int watchdog_fn(void *data)
|
||
|
|
{
|
||
|
|
while (!kthread_should_stop()) {
|
||
|
|
if (guard_enabled) {
|
||
|
|
if (poll_once())
|
||
|
|
push_lock_policy();
|
||
|
|
}
|
||
|
|
msleep(policy.poll_interval_ms > 0 ?
|
||
|
|
policy.poll_interval_ms : 1500);
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- scan ---------- */
|
||
|
|
|
||
|
|
static int run_scan(void)
|
||
|
|
{
|
||
|
|
char *buf;
|
||
|
|
int n;
|
||
|
|
|
||
|
|
buf = kmalloc(SCAN_BUF_SIZE, GFP_KERNEL);
|
||
|
|
if (!buf)
|
||
|
|
return -ENOMEM;
|
||
|
|
|
||
|
|
mutex_lock(&at_mutex);
|
||
|
|
/* AT+COPS=? is slow (up to 180s on cold SIM) */
|
||
|
|
n = at_cmd("AT+COPS=?", buf, SCAN_BUF_SIZE, 180000);
|
||
|
|
mutex_unlock(&at_mutex);
|
||
|
|
|
||
|
|
if (n > 0) {
|
||
|
|
mutex_lock(&scan_mutex);
|
||
|
|
memcpy(scan_buf, buf, n);
|
||
|
|
scan_len = n;
|
||
|
|
mutex_unlock(&scan_mutex);
|
||
|
|
}
|
||
|
|
kfree(buf);
|
||
|
|
return n;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- chardev / ioctl ---------- */
|
||
|
|
|
||
|
|
static ssize_t cg_write(struct file *f, const char __user *u, size_t n, loff_t *ppos)
|
||
|
|
{
|
||
|
|
char cmd[32];
|
||
|
|
size_t len = min(n, sizeof(cmd) - 1);
|
||
|
|
|
||
|
|
if (copy_from_user(cmd, u, len))
|
||
|
|
return -EFAULT;
|
||
|
|
cmd[len] = '\0';
|
||
|
|
while (len > 0 && (cmd[len-1] == '\n' || cmd[len-1] == '\r' ||
|
||
|
|
cmd[len-1] == ' '))
|
||
|
|
cmd[--len] = '\0';
|
||
|
|
|
||
|
|
if (!strcmp(cmd, "on")) {
|
||
|
|
guard_enabled = true;
|
||
|
|
push_lock_policy();
|
||
|
|
} else if (!strcmp(cmd, "off") || !strcmp(cmd, "release")) {
|
||
|
|
guard_enabled = false;
|
||
|
|
release_lock_policy();
|
||
|
|
} else if (!strcmp(cmd, "scan")) {
|
||
|
|
run_scan();
|
||
|
|
} else if (!strcmp(cmd, "relock")) {
|
||
|
|
push_lock_policy();
|
||
|
|
} else if (!strncmp(cmd, "carrier ", 8)) {
|
||
|
|
const char *name = cmd + 8;
|
||
|
|
int i, found = -1;
|
||
|
|
for (i = 0; i < N_CARRIERS; i++) {
|
||
|
|
if (!strcmp(name, carrier_profiles[i].name)) {
|
||
|
|
found = i;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (found < 0)
|
||
|
|
return -EINVAL;
|
||
|
|
current_carrier = found;
|
||
|
|
if (guard_enabled) {
|
||
|
|
/* Re-push full policy so bands apply immediately
|
||
|
|
* (CFUN cycle is needed for band changes). */
|
||
|
|
push_lock_policy();
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
return -EINVAL;
|
||
|
|
}
|
||
|
|
return (ssize_t)n;
|
||
|
|
}
|
||
|
|
|
||
|
|
static long cg_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
|
||
|
|
{
|
||
|
|
switch (cmd) {
|
||
|
|
case CG_IOC_ENABLE:
|
||
|
|
guard_enabled = true;
|
||
|
|
push_lock_policy();
|
||
|
|
return 0;
|
||
|
|
case CG_IOC_DISABLE:
|
||
|
|
guard_enabled = false;
|
||
|
|
release_lock_policy();
|
||
|
|
return 0;
|
||
|
|
case CG_IOC_RELEASE:
|
||
|
|
release_lock_policy();
|
||
|
|
return 0;
|
||
|
|
case CG_IOC_GET_STATUS: {
|
||
|
|
struct cg_status s;
|
||
|
|
|
||
|
|
memset(&s, 0, sizeof(s));
|
||
|
|
s.enabled = guard_enabled ? 1 : 0;
|
||
|
|
s.modem_ok = (modem_filp && !IS_ERR(modem_filp)) ? 1 : 0;
|
||
|
|
strscpy(s.modem_path, modem_path_used, sizeof(s.modem_path));
|
||
|
|
strscpy(s.rat, cur_rat, sizeof(s.rat));
|
||
|
|
s.rat_code = cur_rat_code;
|
||
|
|
s.cipher_known = cur_cipher_known;
|
||
|
|
s.cipher_null = cur_cipher_null;
|
||
|
|
strscpy(s.operator_name, cur_operator, sizeof(s.operator_name));
|
||
|
|
strscpy(s.mcc_mnc, cur_mcc_mnc, sizeof(s.mcc_mnc));
|
||
|
|
s.rsrp = cur_rsrp;
|
||
|
|
s.tx_pwr = cur_pwr_known ? cur_tx_pwr : -140;
|
||
|
|
s.downgrades_blocked = stat_downgrades;
|
||
|
|
s.relocks = stat_relocks;
|
||
|
|
s.polls = stat_polls;
|
||
|
|
if (copy_to_user((void __user *)arg, &s, sizeof(s)))
|
||
|
|
return -EFAULT;
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
case CG_IOC_SCAN: {
|
||
|
|
struct cg_scan *sc;
|
||
|
|
int n;
|
||
|
|
|
||
|
|
sc = kmalloc(sizeof(*sc), GFP_KERNEL);
|
||
|
|
if (!sc)
|
||
|
|
return -ENOMEM;
|
||
|
|
memset(sc, 0, sizeof(*sc));
|
||
|
|
|
||
|
|
n = run_scan();
|
||
|
|
if (n < 0) {
|
||
|
|
sc->status = n;
|
||
|
|
} else {
|
||
|
|
mutex_lock(&scan_mutex);
|
||
|
|
sc->data_len = min_t(uint32_t, scan_len,
|
||
|
|
SCAN_BUF_SIZE - 1);
|
||
|
|
memcpy(sc->data, scan_buf, sc->data_len);
|
||
|
|
sc->data[sc->data_len] = '\0';
|
||
|
|
mutex_unlock(&scan_mutex);
|
||
|
|
sc->status = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (copy_to_user((void __user *)arg, sc, sizeof(*sc))) {
|
||
|
|
kfree(sc);
|
||
|
|
return -EFAULT;
|
||
|
|
}
|
||
|
|
kfree(sc);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
case CG_IOC_SET_POLICY: {
|
||
|
|
struct cg_policy p;
|
||
|
|
if (copy_from_user(&p, (void __user *)arg, sizeof(p)))
|
||
|
|
return -EFAULT;
|
||
|
|
if (p.poll_interval_ms < 200 || p.poll_interval_ms > 60000)
|
||
|
|
return -EINVAL;
|
||
|
|
policy = p;
|
||
|
|
if (guard_enabled)
|
||
|
|
push_lock_policy();
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
return -ENOTTY;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static const struct file_operations cg_fops = {
|
||
|
|
.owner = THIS_MODULE,
|
||
|
|
.write = cg_write,
|
||
|
|
.unlocked_ioctl = cg_ioctl,
|
||
|
|
.compat_ioctl = compat_ptr_ioctl,
|
||
|
|
};
|
||
|
|
|
||
|
|
/* ---------- /proc/cellguard/{status,cells,policy} ---------- */
|
||
|
|
|
||
|
|
static int status_show(struct seq_file *m, void *v)
|
||
|
|
{
|
||
|
|
seq_printf(m, "enabled: %d\n", guard_enabled ? 1 : 0);
|
||
|
|
seq_printf(m, "modem: %s (%s)\n",
|
||
|
|
(modem_filp && !IS_ERR(modem_filp)) ? "up" : "down",
|
||
|
|
modem_path_used);
|
||
|
|
seq_printf(m, "rat: %s (act=%d)\n", cur_rat, cur_rat_code);
|
||
|
|
seq_printf(m, "operator: %s [%s]\n", cur_operator, cur_mcc_mnc);
|
||
|
|
seq_printf(m, "rsrp_dbm: %d\n", cur_rsrp);
|
||
|
|
seq_printf(m, "tx_pwr_dbm: %d\n", cur_pwr_known ? cur_tx_pwr : -140);
|
||
|
|
seq_printf(m, "cipher_known: %d\n", cur_cipher_known);
|
||
|
|
seq_printf(m, "cipher_null: %d\n", cur_cipher_null);
|
||
|
|
seq_printf(m, "block_2g: %d\n", policy.block_2g);
|
||
|
|
seq_printf(m, "block_3g: %d\n", policy.block_3g);
|
||
|
|
seq_printf(m, "block_null: %d\n", policy.block_null_cipher);
|
||
|
|
seq_printf(m, "poll_ms: %d\n", policy.poll_interval_ms);
|
||
|
|
seq_printf(m, "carrier: %s\n",
|
||
|
|
(current_carrier >= 0 && current_carrier < N_CARRIERS) ?
|
||
|
|
carrier_profiles[current_carrier].name : "none");
|
||
|
|
seq_printf(m, "bands_locked: %d\n", bands_locked);
|
||
|
|
seq_printf(m, "bands_method: %s\n",
|
||
|
|
bands_push_method[0] ? bands_push_method : "none");
|
||
|
|
seq_printf(m, "downgrades: %llu\n", stat_downgrades);
|
||
|
|
seq_printf(m, "relocks: %llu\n", stat_relocks);
|
||
|
|
seq_printf(m, "polls: %llu\n", stat_polls);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
static int status_open(struct inode *i, struct file *f)
|
||
|
|
{ return single_open(f, status_show, NULL); }
|
||
|
|
static const struct proc_ops status_pops = {
|
||
|
|
.proc_open = status_open,
|
||
|
|
.proc_read = seq_read,
|
||
|
|
.proc_lseek = seq_lseek,
|
||
|
|
.proc_release = single_release,
|
||
|
|
};
|
||
|
|
|
||
|
|
static int cells_show(struct seq_file *m, void *v)
|
||
|
|
{
|
||
|
|
mutex_lock(&scan_mutex);
|
||
|
|
if (scan_len > 0)
|
||
|
|
seq_write(m, scan_buf, scan_len);
|
||
|
|
else
|
||
|
|
seq_puts(m, "(no scan yet — echo scan > /dev/cellguard)\n");
|
||
|
|
mutex_unlock(&scan_mutex);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
static int cells_open(struct inode *i, struct file *f)
|
||
|
|
{ return single_open(f, cells_show, NULL); }
|
||
|
|
static const struct proc_ops cells_pops = {
|
||
|
|
.proc_open = cells_open,
|
||
|
|
.proc_read = seq_read,
|
||
|
|
.proc_lseek = seq_lseek,
|
||
|
|
.proc_release = single_release,
|
||
|
|
};
|
||
|
|
|
||
|
|
static struct proc_dir_entry *proc_root;
|
||
|
|
|
||
|
|
/* ---------- module init / exit ---------- */
|
||
|
|
|
||
|
|
static int __init cellguard_init(void)
|
||
|
|
{
|
||
|
|
int ret;
|
||
|
|
dev_t dev;
|
||
|
|
|
||
|
|
pr_info("cellguard: init\n");
|
||
|
|
|
||
|
|
/* Resolve GKI-protected symbols before any modem I/O. */
|
||
|
|
ret = cg_resolve_protected();
|
||
|
|
if (ret)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
at_resp_buf = kmalloc(AT_RESP_SIZE, GFP_KERNEL);
|
||
|
|
if (!at_resp_buf)
|
||
|
|
return -ENOMEM;
|
||
|
|
|
||
|
|
modem_filp = open_modem();
|
||
|
|
if (IS_ERR(modem_filp)) {
|
||
|
|
pr_warn("cellguard: no Shannon AT channel found — loaded "
|
||
|
|
"but inert until modem appears\n");
|
||
|
|
modem_filp = NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
|
||
|
|
if (ret < 0)
|
||
|
|
goto err_chrdev;
|
||
|
|
major = MAJOR(dev);
|
||
|
|
|
||
|
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
|
||
|
|
cg_class = class_create(CLASS_NAME);
|
||
|
|
#else
|
||
|
|
cg_class = class_create(THIS_MODULE, CLASS_NAME);
|
||
|
|
#endif
|
||
|
|
if (IS_ERR(cg_class)) { ret = PTR_ERR(cg_class); goto err_class; }
|
||
|
|
|
||
|
|
cdev_init(&cg_cdev, &cg_fops);
|
||
|
|
cg_cdev.owner = THIS_MODULE;
|
||
|
|
ret = cdev_add(&cg_cdev, MKDEV(major, 0), 1);
|
||
|
|
if (ret < 0) goto err_cdev;
|
||
|
|
|
||
|
|
cg_dev = device_create(cg_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
|
||
|
|
if (IS_ERR(cg_dev)) { ret = PTR_ERR(cg_dev); goto err_dev; }
|
||
|
|
|
||
|
|
proc_root = proc_mkdir("cellguard", NULL);
|
||
|
|
if (proc_root) {
|
||
|
|
proc_create("status", 0444, proc_root, &status_pops);
|
||
|
|
proc_create("cells", 0444, proc_root, &cells_pops);
|
||
|
|
}
|
||
|
|
|
||
|
|
watchdog_thread = kthread_run(watchdog_fn, NULL, "cellguard_wd");
|
||
|
|
if (IS_ERR(watchdog_thread)) {
|
||
|
|
ret = PTR_ERR(watchdog_thread);
|
||
|
|
watchdog_thread = NULL;
|
||
|
|
goto err_kth;
|
||
|
|
}
|
||
|
|
|
||
|
|
pr_info("cellguard: /dev/%s ready (major=%d)\n", DEVICE_NAME, major);
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
err_kth:
|
||
|
|
if (proc_root) {
|
||
|
|
remove_proc_entry("status", proc_root);
|
||
|
|
remove_proc_entry("cells", proc_root);
|
||
|
|
proc_remove(proc_root);
|
||
|
|
}
|
||
|
|
device_destroy(cg_class, MKDEV(major, 0));
|
||
|
|
err_dev:
|
||
|
|
cdev_del(&cg_cdev);
|
||
|
|
err_cdev:
|
||
|
|
class_destroy(cg_class);
|
||
|
|
err_class:
|
||
|
|
unregister_chrdev_region(MKDEV(major, 0), 1);
|
||
|
|
err_chrdev:
|
||
|
|
if (modem_filp) filp_close(modem_filp, NULL);
|
||
|
|
kfree(at_resp_buf);
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void __exit cellguard_exit(void)
|
||
|
|
{
|
||
|
|
if (watchdog_thread)
|
||
|
|
kthread_stop(watchdog_thread);
|
||
|
|
|
||
|
|
if (guard_enabled)
|
||
|
|
release_lock_policy();
|
||
|
|
|
||
|
|
if (proc_root) {
|
||
|
|
remove_proc_entry("status", proc_root);
|
||
|
|
remove_proc_entry("cells", proc_root);
|
||
|
|
proc_remove(proc_root);
|
||
|
|
}
|
||
|
|
device_destroy(cg_class, MKDEV(major, 0));
|
||
|
|
cdev_del(&cg_cdev);
|
||
|
|
class_destroy(cg_class);
|
||
|
|
unregister_chrdev_region(MKDEV(major, 0), 1);
|
||
|
|
|
||
|
|
if (modem_filp)
|
||
|
|
filp_close(modem_filp, NULL);
|
||
|
|
|
||
|
|
kfree(at_resp_buf);
|
||
|
|
|
||
|
|
pr_info("cellguard: unloaded (downgrades=%llu, relocks=%llu)\n",
|
||
|
|
stat_downgrades, stat_relocks);
|
||
|
|
}
|
||
|
|
|
||
|
|
module_init(cellguard_init);
|
||
|
|
module_exit(cellguard_exit);
|