diff --git a/.gitignore b/.gitignore index 2f26739..6214692 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ kmod/vpnhide-kmod.zip kmod/.*.cmd kmod/*.usyms kmod/vpnhide_kmod.ko +kmod/.env # Test app test-app/.gradle/ diff --git a/kmod/.env.example b/kmod/.env.example new file mode 100644 index 0000000..e940e5a --- /dev/null +++ b/kmod/.env.example @@ -0,0 +1,8 @@ +# Copy to .env and adjust paths for your setup. +# direnv loads these automatically (see .envrc). + +# Kernel source / prepared headers directory +KERNEL_SRC=~/pixel-kernel-headers + +# Clang toolchain bin directory (must contain clang, ld.lld, llvm-ar, etc.) +CLANG_DIR=~/pixel-kernel/prebuilts/clang/host/linux-x86/clang-r487747c/bin diff --git a/kmod/.envrc b/kmod/.envrc new file mode 100644 index 0000000..2c57cda --- /dev/null +++ b/kmod/.envrc @@ -0,0 +1 @@ +dotenv_if_exists diff --git a/kmod/BUILDING.md b/kmod/BUILDING.md index 899888b..4679ade 100644 --- a/kmod/BUILDING.md +++ b/kmod/BUILDING.md @@ -16,6 +16,7 @@ not already available). - Linux host (Arch, Ubuntu, Debian — anything with make, clang, git) - `adb` connected to the device - `modprobe` on the host (for extracting CRCs from .ko files) +- [`direnv`](https://direnv.net/) (for automatic env var loading) ## Step 1: Identify the GKI generation @@ -38,7 +39,63 @@ the Android version running on the device. A Pixel 7 Pro running Android 16 still has an `android13-5.10` kernel because the generation is frozen at manufacturing time. -## Step 2: Clone the kernel source (shallow, ~500 MB) +## Step 2: Clone the full Pixel kernel tree + +Google ships a single `android14-6.1` kernel covering all Pixels from 6 to +9a. The full tree includes kernel sources, prebuilt toolchain, device trees, +and all out-of-tree Google modules — everything needed with no extra downloads: + +```bash +mkdir ~/pixel-kernel && cd ~/pixel-kernel + +repo init --depth=1 \ + -u https://android.googlesource.com/kernel/manifest \ + -b android-gs-shusky-6.1-android16 + +repo sync -c --no-tags -j$(nproc) +``` + +Then build once to produce all artifacts: + +```bash +./build_shusky.sh # or build_raviole.sh etc. — same kernel image for all +``` + +Set an env var pointing to the tree root — subsequent steps reference it: + +```bash +export PIXEL_KERNEL_TREE=~/pixel-kernel +``` + +### Useful paths within the tree + +| Path | Purpose | +|---|---| +| `$PIXEL_KERNEL_TREE/aosp/` | Full GKI kernel source tree — browse subsystems, understand exported APIs, read in-tree driver implementations | +| `$PIXEL_KERNEL_TREE/out/shusky/dist/kernel-headers.tar.gz` | Kernel headers for building against (extract and use as `KDIR`) | +| `$PIXEL_KERNEL_TREE/out/shusky/dist/vmlinux.symvers` | Symbol versions for `modpost` — use instead of extracting CRCs from device .ko files | +| `$PIXEL_KERNEL_TREE/out/shusky/dist/System.map` | Symbol addresses for debugging | +| `$PIXEL_KERNEL_TREE/private/google-modules/` | Reference implementations of out-of-tree modules (Kconfig, Makefile patterns) | +| `$PIXEL_KERNEL_TREE/prebuilts/clang/host/linux-x86/clang-r487747c/bin` | Bundled clang 17.0.2 — exact compiler used to build the kernel | + +`raviole/dist/` and `shusky/dist/` contain identical `vmlinux.symvers` and +`Image` since it's the same kernel build — use either. + +### Prepare kernel headers for module builds + +```bash +mkdir -p ~/pixel-kernel-headers +tar -xzf $PIXEL_KERNEL_TREE/out/shusky/dist/kernel-headers.tar.gz \ + -C ~/pixel-kernel-headers +cp $PIXEL_KERNEL_TREE/out/shusky/dist/vmlinux.symvers \ + ~/pixel-kernel-headers/Module.symvers +``` + +Then skip to step 3. + +### Alternative: shallow clone (non-Pixel or different GKI generation) + +If you don't need the full tree, clone just the kernel source: ```bash BRANCH="android13-5.10" # ← replace with your generation from step 1 @@ -48,51 +105,97 @@ git clone --depth=1 -b $BRANCH \ ~/kernel-source ``` -This clones only the needed branch with no history — fast and small. +You'll then need to prepare it manually — see [Preparing a standalone kernel source](#preparing-a-standalone-kernel-source) at the end. -## Step 3: Get the Android clang toolchain +--- -If you already have a Pixel kernel tree with prebuilts (e.g. from -building for another device), reuse it: +## Step 3: Configure .env + +The build system uses `direnv` to load `KERNEL_SRC` and `CLANG_DIR` from +a `.env` file. Copy the example and fill in your paths: ```bash -CLANG=~/kernel-tree/prebuilts/clang/host/linux-x86/clang-r*/bin +cd kmod/ +cp .env.example .env ``` -Otherwise, download the standalone toolchain. Google publishes them -at https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86/ -— clone the branch matching your kernel: +Edit `.env`: ```bash -# This is large (~2 GB). If you already have it, skip. -git clone --depth=1 \ - https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86 \ - ~/android-clang -CLANG=~/android-clang/clang-r*/bin +# Pixel kernel tree approach (after extracting headers above): +KERNEL_SRC=~/pixel-kernel-headers +CLANG_DIR=~/pixel-kernel/prebuilts/clang/host/linux-x86/clang-r487747c/bin + +# Or standalone kernel source approach: +# KERNEL_SRC=~/kernel-source +# CLANG_DIR=/path/to/clang/bin ``` -Or use the version bundled with the Pixel kernel tree if you have one. +Allow direnv to load it: -## Step 4: Pull .config from the device +```bash +direnv allow +``` + +From now on, entering the `kmod/` directory automatically exports +`KERNEL_SRC` and `CLANG_DIR`. The Makefile reads both from the environment. + +## Step 4: Build and package + +```bash +make # builds vpnhide_kmod.ko +./build-zip.sh # builds .ko (if needed) + packages vpnhide-kmod.zip +``` + +## Step 5: Install and test + +```bash +adb push vpnhide-kmod.zip /sdcard/Download/ +# Install via KernelSU-Next manager -> Modules -> Install from storage +# Reboot +``` + +After reboot, verify: + +```bash +# Module loaded? +adb shell "su -c 'lsmod | grep vpnhide'" + +# kretprobes registered? +adb shell "su -c 'dmesg | grep vpnhide'" + +# UIDs loaded? +adb shell "su -c 'cat /proc/vpnhide_targets'" +``` + +Pick target apps via the WebUI in KernelSU-Next manager. + +--- + +## Preparing a standalone kernel source + +If you used the shallow clone (non-Pixel path), you need to prepare the +kernel source before building. If you used the Pixel kernel tree with +extracted headers, skip this section entirely. + +### Pull .config from the device ```bash adb shell "su -c 'gzip -d < /proc/config.gz'" > ~/kernel-source/.config ``` -If `/proc/config.gz` doesn't exist, the kernel was built without -`CONFIG_IKCONFIG_PROC`. In that case, use the GKI defconfig: +If `/proc/config.gz` doesn't exist, use the GKI defconfig: ```bash cd ~/kernel-source -make ARCH=arm64 LLVM=1 CC=$CLANG/clang gki_defconfig +make ARCH=arm64 LLVM=1 CC=$CLANG_DIR/clang gki_defconfig ``` -## Step 5: Generate Module.symvers from device's .ko files +### Generate Module.symvers from device .ko files The Module.symvers file contains CRC checksums for every exported kernel symbol. These must match the running kernel exactly -(CONFIG_MODVERSIONS). Extract them from the prebuilt .ko modules -on the device: +(CONFIG_MODVERSIONS): ```bash # Pull all vendor modules from the device @@ -112,13 +215,13 @@ echo "Generated Module.symvers with $(wc -l < ~/kernel-source/Module.symvers) sy ``` Expect 3000-5000 symbols. If you get 0, check that `modprobe` is -installed on your host (`apt install kmod` or `pacman -S kmod`). +installed (`apt install kmod` or `pacman -S kmod`). Alternative: if the device's ROM has a `-kernels` repo on GitHub (e.g. `crdroidandroid/android_device_google_shusky-kernels`), you can download the .ko files from there instead of pulling from device. -## Step 6: Prepare the kernel source +### Prepare headers ```bash cd ~/kernel-source @@ -128,9 +231,9 @@ touch abi_symbollist.raw # Generate headers make ARCH=arm64 LLVM=1 LLVM_IAS=1 \ - CC=$CLANG/clang LD=$CLANG/ld.lld AR=$CLANG/llvm-ar \ - NM=$CLANG/llvm-nm OBJCOPY=$CLANG/llvm-objcopy \ - OBJDUMP=$CLANG/llvm-objdump STRIP=$CLANG/llvm-strip \ + CC=$CLANG_DIR/clang LD=$CLANG_DIR/ld.lld AR=$CLANG_DIR/llvm-ar \ + NM=$CLANG_DIR/llvm-nm OBJCOPY=$CLANG_DIR/llvm-objcopy \ + OBJDUMP=$CLANG_DIR/llvm-objdump STRIP=$CLANG_DIR/llvm-strip \ CROSS_COMPILE=aarch64-linux-gnu- \ olddefconfig prepare ``` @@ -139,24 +242,7 @@ make ARCH=arm64 LLVM=1 LLVM_IAS=1 \ due to host clang version mismatch. This is fine — the module can still build without BTF. Ignore this error. -## Step 7: Generate scripts/module.lds - -```bash -cd ~/kernel-source - -$CLANG/clang -E -Wp,-MD,scripts/.module.lds.d -nostdinc \ - -I arch/arm64/include -I arch/arm64/include/generated \ - -I include -I include/generated \ - -include include/linux/kconfig.h \ - -D__KERNEL__ -DCC_USING_PATCHABLE_FUNCTION_ENTRY \ - --target=aarch64-linux-gnu -x c scripts/module.lds.S \ - 2>/dev/null | grep -v '^#' > scripts/module.lds - -# Fix ARM64 page-size literal that ld.lld can't parse -sed -i 's/((1UL) << 12)/4096/g' scripts/module.lds -``` - -## Step 8: Set UTS_RELEASE (vermagic) +### Set UTS_RELEASE (vermagic) KernelSU bypasses vermagic checks, so any value works if using KSU. For Magisk or manual insmod, the vermagic must match `uname -r`. @@ -165,100 +251,24 @@ For Magisk or manual insmod, the vermagic must match `uname -r`. cd ~/kernel-source # For KernelSU users (any placeholder works): -PLACEHOLDER="6.1.999-placeholder-$(printf 'x%.0s' {1..100})" -echo "#define UTS_RELEASE \"$PLACEHOLDER\"" \ - > include/generated/utsrelease.h -echo -n "$PLACEHOLDER" > include/config/kernel.release +echo '#define UTS_RELEASE "6.1.0-vpnhide"' > include/generated/utsrelease.h +echo -n "6.1.0-vpnhide" > include/config/kernel.release # For Magisk users (must match exactly): KVER="$(adb shell uname -r | tr -d '\r')" -echo "#define UTS_RELEASE \"$KVER\"" \ - > include/generated/utsrelease.h +echo "#define UTS_RELEASE \"$KVER\"" > include/generated/utsrelease.h echo -n "$KVER" > include/config/kernel.release ``` -## Step 9: Build the module +Then set `KERNEL_SRC=~/kernel-source` in your `.env` and proceed to step 4. -```bash -cd /path/to/vpnhide-kmod - -make -C ~/kernel-source M=$(pwd) \ - ARCH=arm64 LLVM=1 LLVM_IAS=1 \ - CC=$CLANG/clang LD=$CLANG/ld.lld \ - AR=$CLANG/llvm-ar NM=$CLANG/llvm-nm \ - OBJCOPY=$CLANG/llvm-objcopy \ - OBJDUMP=$CLANG/llvm-objdump \ - STRIP=$CLANG/llvm-strip \ - CROSS_COMPILE=aarch64-linux-gnu- \ - modules -``` - -Output: `vpnhide_kmod.ko` - -If build fails with "undefined symbol" errors, your Module.symvers -is missing some symbols. Re-extract with more .ko files from the -device, or check that you pulled from the right kernel version. - -## Step 10: Package as KSU module - -```bash -cp vpnhide_kmod.ko module/ -./build-zip.sh -# Output: vpnhide-kmod.zip -``` - -## Step 11: Install and test - -```bash -adb push vpnhide-kmod.zip /sdcard/Download/ -# Install via KernelSU-Next manager → Modules → Install from storage -# Reboot -``` - -After reboot, verify: - -```bash -# Module loaded? -adb shell "su -c 'lsmod | grep vpnhide'" - -# kretprobes registered? -adb shell "su -c 'dmesg | grep vpnhide'" - -# UIDs loaded? -adb shell "su -c 'cat /proc/vpnhide_targets'" -``` - -Pick target apps via the WebUI in KernelSU-Next manager. - -## Quick reference: one-shot build script - -For repeat builds (e.g. after code changes), once the kernel source -is prepared: - -```bash -#!/bin/bash -KSRC=~/kernel-source -CLANG=~/android-clang/clang-r*/bin # adjust path -cd /path/to/vpnhide-kmod -make -C "$KSRC" M=$(pwd) \ - ARCH=arm64 LLVM=1 LLVM_IAS=1 \ - CC=$CLANG/clang LD=$CLANG/ld.lld \ - AR=$CLANG/llvm-ar NM=$CLANG/llvm-nm \ - OBJCOPY=$CLANG/llvm-objcopy \ - OBJDUMP=$CLANG/llvm-objdump \ - STRIP=$CLANG/llvm-strip \ - CROSS_COMPILE=aarch64-linux-gnu- \ - modules -cp vpnhide_kmod.ko module/ -./build-zip.sh -adb push vpnhide-kmod.zip /sdcard/Download/ -``` +--- ## Troubleshooting **`insmod: Exec format error`** - Vermagic mismatch (Magisk doesn't bypass it). Set UTS_RELEASE to - exact `uname -r` value (step 8, Magisk variant). + exact `uname -r` value. - Or Module.symvers CRCs don't match — re-extract from device .ko files. **`insmod: File exists`** @@ -273,15 +283,9 @@ adb push vpnhide-kmod.zip /sdcard/Download/ - Ignore — BTF is optional. The module builds without it. **No `/proc/config.gz`** -- Use `make gki_defconfig` instead (step 4 alternative). +- Use `make gki_defconfig` instead. **kretprobe not firing (ioctl not filtered)** - Check `dmesg | grep vpnhide` for registration messages. - Check `/proc/vpnhide_targets` has the right UIDs. - The target app's UID changes on reinstall — re-resolve via WebUI. - -**NFC payment broken with module active** -- Remove the target app from targets. The kernel module's ioctl - filtering can trigger some anti-tamper SDKs' silent integrity - degradation. Use system_server hooks (vpnhide LSPosed) for - Java-side coverage instead. diff --git a/kmod/Makefile b/kmod/Makefile index 44926d9..5185e4c 100644 --- a/kmod/Makefile +++ b/kmod/Makefile @@ -1,34 +1,35 @@ obj-m += vpnhide_kmod.o -# When invoked from the kernel build system (make -C M=...), -# KERNELRELEASE is set. Only obj-m above matters in that case. -# The rest is for direct invocation (make all / make clean). ifeq ($(KERNELRELEASE),) -KERNEL_SRC ?= /home/okhsunrog/tmp_zfs/kernel_pixel_8pro/aosp -CLANG_DIR ?= /home/okhsunrog/tmp_zfs/kernel_pixel_8pro/prebuilts/clang/host/linux-x86/clang-r487747c +# KERNEL_SRC and CLANG_DIR come from the environment (set via .env + direnv). +ifndef KERNEL_SRC +$(error KERNEL_SRC is not set — copy .env.example to .env and fill in paths) +endif +ifndef CLANG_DIR +$(error CLANG_DIR is not set — copy .env.example to .env and fill in paths) +endif -ARCH := arm64 -CROSS_COMPILE := aarch64-linux-gnu- -CC := $(CLANG_DIR)/bin/clang -LD := $(CLANG_DIR)/bin/ld.lld -AR := $(CLANG_DIR)/bin/llvm-ar -NM := $(CLANG_DIR)/bin/llvm-nm -OBJCOPY := $(CLANG_DIR)/bin/llvm-objcopy -OBJDUMP := $(CLANG_DIR)/bin/llvm-objdump -STRIP := $(CLANG_DIR)/bin/llvm-strip +ARCH := arm64 +CROSS_COMPILE := aarch64-linux-gnu- +CC := $(CLANG_DIR)/clang +LD := $(CLANG_DIR)/ld.lld +AR := $(CLANG_DIR)/llvm-ar +NM := $(CLANG_DIR)/llvm-nm +OBJCOPY := $(CLANG_DIR)/llvm-objcopy +OBJDUMP := $(CLANG_DIR)/llvm-objdump +STRIP := $(CLANG_DIR)/llvm-strip + +MAKE_ARGS := -C $(KERNEL_SRC) M=$(CURDIR) \ + ARCH=$(ARCH) LLVM=1 LLVM_IAS=1 \ + CROSS_COMPILE=$(CROSS_COMPILE) \ + CC=$(CC) LD=$(LD) AR=$(AR) NM=$(NM) \ + OBJCOPY=$(OBJCOPY) OBJDUMP=$(OBJDUMP) STRIP=$(STRIP) all: - $(MAKE) -C $(KERNEL_SRC) M=$(PWD) \ - ARCH=$(ARCH) \ - CROSS_COMPILE=$(CROSS_COMPILE) \ - CC=$(CC) LD=$(LD) AR=$(AR) NM=$(NM) \ - OBJCOPY=$(OBJCOPY) OBJDUMP=$(OBJDUMP) STRIP=$(STRIP) \ - modules + $(MAKE) $(MAKE_ARGS) modules clean: - $(MAKE) -C $(KERNEL_SRC) M=$(PWD) \ - ARCH=$(ARCH) \ - clean + $(MAKE) $(MAKE_ARGS) clean endif diff --git a/kmod/build-zip.sh b/kmod/build-zip.sh index a9fa5da..0675b85 100755 --- a/kmod/build-zip.sh +++ b/kmod/build-zip.sh @@ -3,21 +3,10 @@ set -euo pipefail cd "$(dirname "$0")" -# Build the kernel module if .ko is not fresh -KSRC="${KSRC:-$HOME/tmp_zfs/kernel_build/kernel-source}" -CLANG="${CLANG:-$HOME/Android/Sdk/ndk/29.0.14033849/toolchains/llvm/prebuilt/linux-x86_64/bin}" - +# Build the kernel module (env vars loaded by direnv from .env) if [ ! -f vpnhide_kmod.ko ] || [ vpnhide_kmod.c -nt vpnhide_kmod.ko ]; then echo "Building kernel module..." - make -C "$KSRC" M=$(pwd) \ - ARCH=arm64 LLVM=1 LLVM_IAS=1 \ - CC=$CLANG/clang LD=$CLANG/ld.lld \ - AR=$CLANG/llvm-ar NM=$CLANG/llvm-nm \ - OBJCOPY=$CLANG/llvm-objcopy \ - OBJDUMP=$CLANG/llvm-objdump \ - STRIP=$CLANG/llvm-strip \ - CROSS_COMPILE=aarch64-linux-gnu- \ - modules + make fi cp vpnhide_kmod.ko module/vpnhide_kmod.ko diff --git a/kmod/vpnhide_kmod.c b/kmod/vpnhide_kmod.c index 033d34c..5881932 100644 --- a/kmod/vpnhide_kmod.c +++ b/kmod/vpnhide_kmod.c @@ -8,10 +8,9 @@ * works on stock Android GKI kernels with CONFIG_KPROBES=y. * * Hooks: - * - dev_ioctl: filters SIOCGIFFLAGS / SIOCGIFNAME / SIOCGIFCONF - * - rtnl_dump_ifinfo: filters RTM_NEWLINK netlink dumps (getifaddrs) + * - dev_ioctl: filters SIOCGIFFLAGS / SIOCGIFNAME + * - rtnl_fill_ifinfo: filters RTM_NEWLINK netlink dumps (getifaddrs) * - fib_route_seq_show: filters /proc/net/route entries - * - tcp4_seq_show: filters /proc/net/tcp entries * * Target UIDs are written to /proc/vpnhide_targets from userspace. */ @@ -165,23 +164,29 @@ static const struct proc_ops targets_proc_ops = { /* ================================================================== */ /* Hook 1: dev_ioctl — SIOCGIFFLAGS / SIOCGIFNAME */ +/* */ +/* dev_ioctl() on GKI 6.1: */ +/* int dev_ioctl(struct net *net, unsigned int cmd, */ +/* struct ifreq *ifr, void __user *data, */ +/* bool *need_copyout) */ +/* arm64: x0=net, x1=cmd, x2=ifr (KERNEL ptr), x3=data (__user) */ +/* */ +/* Note: SIOCGIFCONF goes through sock_ioctl -> dev_ifconf, not */ +/* through dev_ioctl, so it is not covered here. */ /* ================================================================== */ struct dev_ioctl_data { unsigned int cmd; - void __user *arg; + struct ifreq *kifr; /* kernel pointer, saved from x2 */ }; static int dev_ioctl_entry(struct kretprobe_instance *ri, struct pt_regs *regs) { struct dev_ioctl_data *data = (void *)ri->data; - unsigned int cmd; - /* arm64: x0=net, x1=cmd, x2=arg */ - cmd = (unsigned int)regs->regs[1]; - data->cmd = cmd; - data->arg = (void __user *)regs->regs[2]; + data->cmd = (unsigned int)regs->regs[1]; + data->kifr = (struct ifreq *)regs->regs[2]; if (!is_target_uid()) data->cmd = 0; @@ -193,92 +198,27 @@ static int dev_ioctl_ret(struct kretprobe_instance *ri, struct pt_regs *regs) { struct dev_ioctl_data *data = (void *)ri->data; - long ret = regs_return_value(regs); + char name[IFNAMSIZ]; - if (data->cmd == 0 || ret != 0) + if (data->cmd == 0 || regs_return_value(regs) != 0) + return 0; + + if (data->cmd != SIOCGIFFLAGS && data->cmd != SIOCGIFNAME) return 0; /* - * dev_ioctl() signature on GKI 6.1: - * int dev_ioctl(struct net *net, unsigned int cmd, - * struct ifreq *ifr, void __user *data, - * bool *need_copyout) - * - * x2 = ifr is a KERNEL pointer (the caller already did - * copy_from_user into a stack-local ifreq). We must NOT - * use copy_from_user on it — ARM64 PAN would EFAULT. - * Read via direct pointer dereference instead. - * - * x3 = data is the original __user pointer. For SIOCGIFCONF - * we need this to patch the userspace buffer. + * ifr (x2) is a KERNEL pointer — the caller already did + * copy_from_user into a stack-local ifreq. Read via direct + * dereference; copy_from_user would EFAULT under ARM64 PAN. */ + if (!data->kifr) + return 0; - switch (data->cmd) { - case SIOCGIFFLAGS: - case SIOCGIFNAME: { - struct ifreq *kifr = (struct ifreq *)data->arg; - char name[IFNAMSIZ]; + memcpy(name, data->kifr->ifr_name, IFNAMSIZ); + name[IFNAMSIZ - 1] = '\0'; - if (!kifr) - break; - memcpy(name, kifr->ifr_name, IFNAMSIZ); - name[IFNAMSIZ - 1] = '\0'; - if (is_vpn_ifname(name)) - regs_set_return_value(regs, -ENODEV); - break; - } - - case SIOCGIFCONF: { - /* - * SIOCGIFCONF is handled in sock_ioctl → dev_ifconf, - * which has a different call path (not through dev_ioctl - * on GKI 6.1). This case is kept for completeness but - * may not fire. The actual SIOCGIFCONF filtering is - * handled by a separate hook if needed. - * - * For SIOCGIFCONF that DOES come through dev_ioctl on - * some kernels: the ifconf is in userspace, so we use - * the __user pointer from x3. - */ - void __user *udata = (void __user *)regs->regs[3]; - struct ifconf ifc; - struct ifreq __user *usr_ifr; - struct ifreq tmp; - int i, n, dst; - - if (!udata) - break; - if (copy_from_user(&ifc, udata, sizeof(ifc))) - break; - if (!ifc.ifc_req || ifc.ifc_len <= 0) - break; - - n = ifc.ifc_len / (int)sizeof(struct ifreq); - usr_ifr = ifc.ifc_req; - dst = 0; - - for (i = 0; i < n; i++) { - if (copy_from_user(&tmp, &usr_ifr[i], sizeof(tmp))) - break; - tmp.ifr_name[IFNAMSIZ - 1] = '\0'; - if (is_vpn_ifname(tmp.ifr_name)) - continue; - if (dst != i) { - if (copy_to_user(&usr_ifr[dst], &tmp, - sizeof(tmp))) - break; - } - dst++; - } - - if (dst < n) { - ifc.ifc_len = dst * (int)sizeof(struct ifreq); - if (copy_to_user(udata, &ifc, sizeof(ifc))) - break; - } - break; - } - } + if (is_vpn_ifname(name)) + regs_set_return_value(regs, -ENODEV); return 0; } @@ -349,81 +289,100 @@ static struct kretprobe rtnl_fill_krp = { /* ================================================================== */ /* Hook 3: fib_route_seq_show — /proc/net/route */ /* */ -/* Each line in /proc/net/route starts with the interface name. If */ -/* it's a VPN interface and the caller is a target, skip the line */ -/* by returning 0 without printing (SEQ_SKIP equivalent). */ +/* fib_route_seq_show(struct seq_file *seq, void *v) writes one or */ +/* more tab-separated route lines into seq->buf, each ending with */ +/* '\n'. The first field is the interface name. */ +/* */ +/* We save seq and seq->count on entry. In the return handler we */ +/* scan what was written, compact out VPN lines, and adjust count. */ /* ================================================================== */ +struct fib_route_data { + struct seq_file *seq; + size_t start_count; + bool target; +}; + static int fib_route_entry(struct kretprobe_instance *ri, struct pt_regs *regs) { - /* We only need to filter in the return handler. Mark in entry - * whether this is a target UID to avoid re-checking. */ - *(bool *)ri->data = is_target_uid(); + struct fib_route_data *data = (void *)ri->data; + + /* + * arm64: x0 = seq_file*, x1 = v (iterator element). + * Save seq pointer and current buffer position so the + * return handler knows where this call's output begins. + */ + data->seq = (struct seq_file *)regs->regs[0]; + data->target = is_target_uid(); + + if (data->target && data->seq) + data->start_count = data->seq->count; + else + data->start_count = 0; + return 0; } static int fib_route_ret(struct kretprobe_instance *ri, struct pt_regs *regs) { - bool target = *(bool *)ri->data; - struct seq_file *seq; - const char *buf; - int i; + struct fib_route_data *data = (void *)ri->data; + struct seq_file *seq = data->seq; + char *buf, *src, *dst, *end; + char ifname[IFNAMSIZ]; + int j; - if (!target) + if (!data->target || !seq || !seq->buf) + return 0; + + if (seq->count <= data->start_count) return 0; /* - * After fib_route_seq_show returns, seq_file has the line in - * seq->buf at position seq->count - (length of last line). - * We check the last written line for a VPN interface name. + * Scan the region [start_count, seq->count) for lines whose + * first tab-separated field is a VPN interface name. Compact + * out matching lines in place and adjust seq->count. * - * arm64: x0 = seq_file* + * Each route line looks like: "tun0\t08000000\t...\n" */ - seq = (struct seq_file *)regs->regs[0]; - if (!seq || seq->count == 0) - return 0; - - /* The route line starts at the beginning of what was just - * written. Find the last newline before seq->count to - * locate the start of the current line. */ buf = seq->buf; - if (!buf) - return 0; + src = buf + data->start_count; + dst = src; + end = buf + seq->count; - /* Scan the interface name field (first field, tab-separated). */ - for (i = 0; i < IFNAMSIZ && i < seq->count; i++) { - if (buf[seq->count - 1 - i] == '\n' || i == 0) { - const char *line_start; - char ifname[IFNAMSIZ]; - int j; + while (src < end) { + char *nl = memchr(src, '\n', end - src); + char *line_end = nl ? nl + 1 : end; + size_t line_len = line_end - src; - if (i == 0 && seq->count > 1) - continue; + /* Extract the interface name (first field, tab-delimited) */ + for (j = 0; j < IFNAMSIZ - 1 && j < (int)line_len && + src[j] != '\t' && src[j] != '\n'; j++) + ifname[j] = src[j]; + ifname[j] = '\0'; - line_start = (i == 0) ? buf : buf + seq->count - i; - for (j = 0; j < IFNAMSIZ - 1 && line_start[j] && - line_start[j] != '\t' && line_start[j] != ' '; - j++) - ifname[j] = line_start[j]; - ifname[j] = '\0'; - - if (is_vpn_ifname(ifname)) { - /* Rewind seq->count to hide this line */ - seq->count -= (i == 0) ? seq->count : i; - } - break; + if (is_vpn_ifname(ifname)) { + /* Skip this line */ + src = line_end; + continue; } + + /* Keep this line — move it down if there's a gap */ + if (dst != src) + memmove(dst, src, line_len); + dst += line_len; + src = line_end; } + seq->count = dst - buf; return 0; } static struct kretprobe fib_route_krp = { .handler = fib_route_ret, .entry_handler = fib_route_entry, - .data_size = sizeof(bool), + .data_size = sizeof(struct fib_route_data), .maxactive = 20, .kp.symbol_name = "fib_route_seq_show", }; diff --git a/test-app/app/src/main/java/dev/okhsunrog/vpnhide/test/MainActivity.kt b/test-app/app/src/main/java/dev/okhsunrog/vpnhide/test/MainActivity.kt index 54c96d4..f4d66e4 100644 --- a/test-app/app/src/main/java/dev/okhsunrog/vpnhide/test/MainActivity.kt +++ b/test-app/app/src/main/java/dev/okhsunrog/vpnhide/test/MainActivity.kt @@ -59,8 +59,17 @@ fun VpnHideTestApp() { MaterialTheme(colorScheme = colorScheme) { val context = LocalContext.current + val cm = context.getSystemService(ConnectivityManager::class.java) var results by remember { mutableStateOf>(emptyList()) } - var summary by remember { mutableStateOf("Tap 'Run All Checks' to start") } + var summary by remember { mutableStateOf("Running...") } + + LaunchedEffect(Unit) { + val r = runAllChecks(cm) + results = r + val scored = r.filter { it.passed != null } + val passed = scored.count { it.passed == true } + summary = "$passed/${scored.size} passed" + } Scaffold( topBar = { @@ -90,7 +99,7 @@ fun VpnHideTestApp() { Button( onClick = { - val cm = context.getSystemService(ConnectivityManager::class.java) + summary = "Running..." val r = runAllChecks(cm) results = r val scored = r.filter { it.passed != null }