#!/system/bin/sh # cellguard — CLI driver for the cellguard kernel module. # # Invoked by the KernelSU-Next WebUI through ksu.exec("cellguard "). # Every subcommand prints a single JSON object on stdout so the WebUI # can JSON.parse() the result directly. CONFIG_DIR="/data/adb/cellguard" CONFIG_FILE="$CONFIG_DIR/config.sh" DEV=/dev/cellguard mkdir -p "$CONFIG_DIR" [ -f "$CONFIG_FILE" ] || cat > "$CONFIG_FILE" << 'EOF' ENABLED=1 BLOCK_2G=1 BLOCK_3G=1 BLOCK_NULL_CIPHER=1 POLL_MS=1500 CARRIER=none EOF . "$CONFIG_FILE" [ -z "$CARRIER" ] && CARRIER=none VALID_CARRIERS="none tmobile verizon att uscellular dish cricket metro googlefi generic_us" # JSON escaping — strings are put through sed to escape \ and " j_esc() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \ -e ':a;N;$!ba;s/\n/\\n/g' } emit_error() { printf '{"ok":false,"error":"%s"}\n' "$(j_esc "$1")" exit 1 } require_dev() { [ -c "$DEV" ] || emit_error "kernel module not loaded (/dev/cellguard missing)" } save_config() { cat > "$CONFIG_FILE" << EOF ENABLED=$ENABLED BLOCK_2G=$BLOCK_2G BLOCK_3G=$BLOCK_3G BLOCK_NULL_CIPHER=$BLOCK_NULL_CIPHER POLL_MS=$POLL_MS CARRIER=$CARRIER EOF } ##################################################################### # status — read /proc/cellguard/status and reformat as JSON ##################################################################### cmd_status() { require_dev if [ ! -f /proc/cellguard/status ]; then emit_error "/proc/cellguard/status missing" fi # /proc/cellguard/status is key: value, one per line awk -v cfg2g="$BLOCK_2G" -v cfg3g="$BLOCK_3G" \ -v cfgnull="$BLOCK_NULL_CIPHER" -v cfgpoll="$POLL_MS" ' BEGIN { FS=":"; printf "{" ; first=1 } { k=$1; sub(/^ +/,"",k); sub(/ +$/,"",k) v=$2; for (i=3;i<=NF;i++) v=v ":" $i sub(/^ +/,"",v); sub(/ +$/,"",v) gsub(/\\/,"\\\\",v); gsub(/"/,"\\\"",v) if (!first) printf "," first=0 # numeric fields if (k=="enabled" || k=="rat_code" || k=="cipher_known" || \ k=="cipher_null" || k=="block_2g" || k=="block_3g" || \ k=="block_null" || k=="poll_ms" || k=="rsrp_dbm" || \ k=="tx_pwr_dbm" || \ k=="downgrades" || k=="relocks" || k=="polls" || \ k=="bands_locked") { # rat field might be mixed — strip trailing junk n=v; sub(/[^-0-9].*/,"",n); if (n=="") n=0 printf "\"%s\":%s", k, n } else if (k=="modem" || k=="rat" || k=="operator" || \ k=="carrier" || k=="bands_method") { printf "\"%s\":\"%s\"", k, v } else { printf "\"%s\":\"%s\"", k, v } } END { printf ",\"ok\":true}" } ' /proc/cellguard/status echo } ##################################################################### # on / off — flip the guard, persist to config ##################################################################### cmd_on() { require_dev echo "on" > "$DEV" 2>/dev/null || emit_error "write /dev/cellguard failed" ENABLED=1 save_config printf '{"ok":true,"enabled":1}\n' } cmd_off() { require_dev echo "off" > "$DEV" 2>/dev/null || emit_error "write /dev/cellguard failed" ENABLED=0 save_config printf '{"ok":true,"enabled":0}\n' } ##################################################################### # apply-config — push saved policy, then on/off accordingly # (called by service.sh at boot, and by any config-change flow) ##################################################################### cmd_apply_config() { require_dev # Select the carrier profile first so that when we flip the guard on, # the kernel's full push_lock_policy() applies WS46 + band-lock in # one CFUN cycle. When the guard is off we still record the carrier # for next enable. if [ -n "$CARRIER" ] && [ "$CARRIER" != "none" ]; then echo "carrier $CARRIER" > "$DEV" 2>/dev/null fi if [ "$ENABLED" = "1" ]; then echo "on" > "$DEV" 2>/dev/null else echo "off" > "$DEV" 2>/dev/null fi printf '{"ok":true,"applied":true,"enabled":%s,"carrier":"%s"}\n' \ "$ENABLED" "$CARRIER" } ##################################################################### # relock — force an immediate policy push even if state looks fine ##################################################################### cmd_relock() { require_dev echo "relock" > "$DEV" 2>/dev/null || emit_error "relock write failed" printf '{"ok":true,"relocked":true}\n' } ##################################################################### # scan — trigger AT+COPS=?, parse result, emit JSON list # # AT+COPS=? response: # +COPS: (stat,"long","short","numeric",AcT),(...),... # stat: 0=unknown 1=available 2=current 3=forbidden # AcT : 0/1/3/8 = 2G, 2/4-6 = 3G, 7/9/10 = LTE, 11/13 = NR ##################################################################### cmd_scan() { require_dev # Kick the scan and wait. AT+COPS=? can take up to ~180s on a cold # SIM; the module runs it synchronously in the ioctl path. echo "scan" > "$DEV" 2>/dev/null || emit_error "scan write failed" # Poll /proc/cellguard/cells until it has a +COPS: line or we time out n=0 while [ $n -lt 200 ]; do if grep -q "+COPS:" /proc/cellguard/cells 2>/dev/null; then break fi sleep 1 n=$((n + 1)) done raw=$(cat /proc/cellguard/cells 2>/dev/null) [ -z "$raw" ] && emit_error "scan produced no output" printf '%s' "$raw" | awk ' BEGIN { printf "{\"ok\":true,\"cells\":[" first=1 } { line=$0 # Work through each (...) tuple while (match(line, /\([^()]+\)/)) { tup=substr(line, RSTART+1, RLENGTH-2) line=substr(line, RSTART+RLENGTH) # Split tup on commas, but ignore commas inside quotes n=0 buf="" inq=0 for (i=1;i<=length(tup);i++) { c=substr(tup,i,1) if (c=="\"") { inq=1-inq; continue } if (c=="," && !inq) { n++; f[n]=buf; buf=""; continue } buf = buf c } n++; f[n]=buf # A scan tuple has 5 fields: stat,long,short,numeric,act # The trailing "supported modes" / "supported formats" tuples # have 1-2 numeric fields — skip those. if (n < 4) continue stat=f[1]; longn=f[2]; shortn=f[3]; num=f[4]; act=f[5] if (stat !~ /^[0-9]+$/) continue # Map AcT to RAT name and a security-ranking score act_n = act + 0 rat="?"; score=0 if (act_n==0||act_n==1||act_n==3||act_n==8) { rat="2G"; score=1 } else if (act_n==2||act_n==4||act_n==5||act_n==6) { rat="3G"; score=2 } else if (act_n==7||act_n==9||act_n==10) { rat="LTE"; score=3 } else if (act_n==11||act_n==13) { rat="5G-SA"; score=4 } avail="unknown" if (stat=="0") avail="unknown" else if (stat=="1") avail="available" else if (stat=="2") avail="current" else if (stat=="3") avail="forbidden" # Escape gsub(/\\/,"\\\\",longn); gsub(/"/,"\\\"",longn) gsub(/\\/,"\\\\",shortn); gsub(/"/,"\\\"",shortn) gsub(/\\/,"\\\\",num); gsub(/"/,"\\\"",num) if (!first) printf "," first=0 printf "{\"operator\":\"%s\",\"short\":\"%s\",\"mcc_mnc\":\"%s\",\"rat\":\"%s\",\"act\":%d,\"avail\":\"%s\",\"score\":%d}", \ longn, shortn, num, rat, act_n, avail, score } } END { printf "]}\n" } ' } ##################################################################### # carrier — set the band-lock profile # cellguard carrier list # cellguard carrier ##################################################################### cmd_carrier() { name="$1" if [ -z "$name" ] || [ "$name" = "list" ]; then printf '{"ok":true,"current":"%s","carriers":[' "$CARRIER" first=1 for c in $VALID_CARRIERS; do if [ "$first" = "1" ]; then first=0; else printf ','; fi printf '"%s"' "$c" done printf ']}\n' return fi ok=0 for c in $VALID_CARRIERS; do [ "$c" = "$name" ] && ok=1 done [ "$ok" = "1" ] || emit_error "unknown carrier: $name" require_dev echo "carrier $name" > "$DEV" 2>/dev/null || emit_error "write failed" CARRIER="$name" save_config printf '{"ok":true,"carrier":"%s"}\n' "$name" } ##################################################################### # config — read/write persisted policy from the WebUI # # Usage: # cellguard config get # cellguard config set block_2g=1 block_3g=0 block_null_cipher=1 poll_ms=2000 ##################################################################### cmd_config() { sub="$1"; shift case "$sub" in get|"") printf '{"ok":true,"enabled":%s,"block_2g":%s,"block_3g":%s,"block_null_cipher":%s,"poll_ms":%s,"carrier":"%s"}\n' \ "$ENABLED" "$BLOCK_2G" "$BLOCK_3G" "$BLOCK_NULL_CIPHER" "$POLL_MS" "$CARRIER" ;; set) while [ -n "$1" ]; do k="${1%%=*}" v="${1#*=}" case "$k" in enabled) ENABLED="$v" ;; block_2g) BLOCK_2G="$v" ;; block_3g) BLOCK_3G="$v" ;; block_null_cipher) BLOCK_NULL_CIPHER="$v" ;; poll_ms) POLL_MS="$v" ;; *) emit_error "unknown key: $k" ;; esac shift done save_config cmd_apply_config ;; *) emit_error "config: unknown subcommand '$sub'" ;; esac } ##################################################################### # dispatch ##################################################################### case "$1" in status) cmd_status ;; on) cmd_on ;; off) cmd_off ;; relock) cmd_relock ;; scan) cmd_scan ;; carrier) shift; cmd_carrier "$@" ;; apply-config) cmd_apply_config ;; config) shift; cmd_config "$@" ;; *) cat << USAGE cellguard — driver for /dev/cellguard cellguard status show current RAT, operator, cipher, counters cellguard on enable the guard (force LTE/NR, re-lock on drop) cellguard off disable and release the modem cellguard relock force one relock pass now cellguard scan full AT+COPS=? scan, JSON list of visible networks cellguard config get|set read or update persisted policy All subcommands emit a single-line JSON object on stdout. USAGE ;; esac