Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cargo/android-ebpf-config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[patch.crates-io]
libbpf-sys = { path = "third_party/libbpf-sys-android" }
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
debug/
target/

pcap.pc

.cargo/
!.cargo/
!.cargo/android-ebpf-config.toml

# These are backup files generated by rustfmt
**/*.rs.bk

Expand All @@ -22,3 +28,10 @@ target/
.aider*
/logs
.venv

#Artifact
libpcap.pc

third_party/

.cargo/config.toml
16 changes: 15 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ test = false
[dependencies]
anyhow = "1.0"
libc = "0.2"
arboard = { version = "3.6", features = ["wayland-data-control"] }
# arboard (clipboard) not available on Android - added in target-specific section below
crossterm = "0.29"
crossbeam = "0.8"
dashmap = "6.1"
Expand All @@ -51,12 +51,21 @@ flate2 = "1"
maxminddb = "0.28"
regex-lite = "0.1"

# Clipboard support for non-Android platforms
[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { version = "3.6", features = ["wayland-data-control"] }

[target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.18"
libbpf-rs = { version = "0.26", optional = true }
landlock = { version = "0.4", optional = true }
caps = { version = "0.5", optional = true }

# Android uses Linux platform module but without eBPF/landlock
[target.'cfg(target_os = "android")'.dependencies]
procfs = "0.18"
libbpf-rs = { version = "0.26", optional = true, features = ["vendored"] }

[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = [
"Win32_Foundation",
Expand Down Expand Up @@ -96,6 +105,9 @@ windows = { version = "0.62", features = [
[target.'cfg(target_os = "linux")'.build-dependencies]
libbpf-cargo = { version = "0.26", optional = true }

[target.'cfg(target_os = "android")'.build-dependencies]
libbpf-cargo = { version = "0.26", optional = true }

[features]
# eBPF is enabled by default for enhanced performance on Linux.
# On non-Linux platforms, this feature has no effect as all eBPF code
Expand All @@ -105,6 +117,7 @@ libbpf-cargo = { version = "0.26", optional = true }
default = ["ebpf", "landlock", "macos-sandbox"]
linux-default = ["ebpf"] # Deprecated: kept for backwards compatibility
ebpf = ["libbpf-rs", "dep:libbpf-cargo"]
android-ebpf = ["libbpf-rs", "dep:libbpf-cargo"]
landlock = ["dep:landlock", "dep:caps"]
macos-sandbox = []

Expand Down Expand Up @@ -202,6 +215,7 @@ harness = false
name = "rate_tracker"
harness = false


[profile.release]
lto = "thin"
codegen-units = 1
Expand Down
16 changes: 12 additions & 4 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ fn main() -> Result<()> {
// Compile eBPF programs on Linux when the feature is enabled
// Check TARGET environment variable, not cfg!, to handle cross-compilation correctly
let target = env::var("TARGET").unwrap_or_default();
if target.contains("linux") && env::var("CARGO_FEATURE_EBPF").is_ok() {
let build_ebpf = if target.contains("android") {
env::var("CARGO_FEATURE_ANDROID_EBPF").is_ok()
} else if target.contains("linux") {
env::var("CARGO_FEATURE_EBPF").is_ok()
} else {
false
};

if build_ebpf {
compile_ebpf_programs();
}

Expand Down Expand Up @@ -186,7 +194,7 @@ fn download_windows_npcap_sdk() -> Result<()> {
Ok(())
}

#[cfg(all(target_os = "linux", feature = "ebpf"))]
#[cfg(any(feature = "ebpf", feature = "android-ebpf"))]
fn get_vmlinux_header(arch: &str) -> Result<PathBuf> {
// Use bundled vmlinux.h from resources directory
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
Expand All @@ -205,7 +213,7 @@ fn get_vmlinux_header(arch: &str) -> Result<PathBuf> {
}
}

#[cfg(all(target_os = "linux", feature = "ebpf"))]
#[cfg(any(feature = "ebpf", feature = "android-ebpf"))]
fn compile_ebpf_programs() {
use libbpf_cargo::SkeletonBuilder;
use std::ffi::OsStr;
Expand Down Expand Up @@ -247,7 +255,7 @@ fn compile_ebpf_programs() {
println!("cargo:rerun-if-changed={}", src);
}

#[cfg(not(all(target_os = "linux", feature = "ebpf")))]
#[cfg(not(any(feature = "ebpf", feature = "android-ebpf")))]
fn compile_ebpf_programs() {
// No-op when not on Linux or eBPF feature is not enabled
}
155 changes: 155 additions & 0 deletions scripts/cargo-android.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
NDK_ROOT=${RUSTNET_ANDROID_NDK:-${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}}
ANDROID_TARGET=${RUSTNET_ANDROID_TARGET:-aarch64-linux-android}
ANDROID_API=${RUSTNET_ANDROID_API:-}
LIBPCAP_ROOT=${RUSTNET_ANDROID_LIBPCAP_ROOT:-${ROOT_DIR}/../libpcap-install}
LIBPCAP_VER=${RUSTNET_ANDROID_LIBPCAP_VER:-1.10.5}
ENABLE_ANDROID_EBPF=${RUSTNET_ANDROID_ENABLE_EBPF:-1}
ANDROID_EBPF_CONFIG="${ROOT_DIR}/.cargo/android-ebpf-config.toml"

if [[ -z "${NDK_ROOT}" ]]; then
cat >&2 <<'EOF'
Android NDK path is required.

Set one of:
RUSTNET_ANDROID_NDK
ANDROID_NDK_HOME
ANDROID_NDK_ROOT
EOF
exit 1
fi

find_prebuilt_dir() {
local prebuilt_root="${NDK_ROOT}/toolchains/llvm/prebuilt"
local host_tag=${RUSTNET_ANDROID_HOST_TAG:-}

if [[ -n "${host_tag}" ]]; then
echo "${prebuilt_root}/${host_tag}"
return
fi

case "$(uname -s)-$(uname -m)" in
Linux-x86_64) host_tag="linux-x86_64" ;;
Darwin-arm64) host_tag="darwin-x86_64" ;;
Darwin-x86_64) host_tag="darwin-x86_64" ;;
*)
host_tag=$(find "${prebuilt_root}" -mindepth 1 -maxdepth 1 -type d | head -n 1)
echo "${host_tag}"
return
;;
esac

echo "${prebuilt_root}/${host_tag}"
}

default_api_for_target() {
case "$1" in
aarch64-linux-android|x86_64-linux-android|armv7-linux-androideabi|i686-linux-android) echo 24 ;;
*) echo 24 ;;
esac
}

PREBUILT_DIR=$(find_prebuilt_dir)
TOOLCHAIN_DIR="${PREBUILT_DIR}/bin"

if [[ ! -d "${TOOLCHAIN_DIR}" ]]; then
echo "Android NDK toolchain directory not found: ${TOOLCHAIN_DIR}" >&2
exit 1
fi

if [[ -z "${ANDROID_API}" ]]; then
ANDROID_API=$(default_api_for_target "${ANDROID_TARGET}")
fi

if (( ANDROID_API < 24 )); then
echo "Android API ${ANDROID_API} is too low for current libpcap/pnet support; using 24 instead." >&2
ANDROID_API=24
fi

CLANG="${TOOLCHAIN_DIR}/${ANDROID_TARGET}${ANDROID_API}-clang"
AR="${TOOLCHAIN_DIR}/llvm-ar"

if [[ ! -x "${CLANG}" ]]; then
echo "Android clang not found: ${CLANG}" >&2
echo "Available ${ANDROID_TARGET} toolchains:" >&2
find "${TOOLCHAIN_DIR}" -maxdepth 1 -type f -name "${ANDROID_TARGET}*-clang" -printf ' %f\n' | sort >&2 || true
exit 1
fi

if [[ ! -x "${AR}" ]]; then
echo "Android llvm-ar not found: ${AR}" >&2
exit 1
fi

if [[ ! -d "${LIBPCAP_ROOT}" ]]; then
echo "Android libpcap root not found: ${LIBPCAP_ROOT}" >&2
exit 1
fi

if [[ ! -d "${LIBPCAP_ROOT}/lib" ]]; then
echo "Android libpcap library directory not found: ${LIBPCAP_ROOT}/lib" >&2
exit 1
fi

if [[ ! -d "${LIBPCAP_ROOT}/include" ]]; then
echo "Android libpcap include directory not found: ${LIBPCAP_ROOT}/include" >&2
exit 1
fi

target_env=${ANDROID_TARGET//-/_}
export "CC_${target_env}=${CLANG}"
export "AR_${target_env}=${AR}"
export "CARGO_TARGET_${target_env^^}_LINKER=${CLANG}"
export "CARGO_TARGET_${target_env^^}_AR=${AR}"
export LIBPCAP_LIBDIR="${LIBPCAP_ROOT}/lib"
export LIBPCAP_VER="${LIBPCAP_VER}"

cat >&2 <<EOF
Android target: ${ANDROID_TARGET}
Android API: ${ANDROID_API}
NDK prebuilt: ${PREBUILT_DIR}
Clang: ${CLANG}
libpcap: ${LIBPCAP_ROOT}
android-ebpf: ${ENABLE_ANDROID_EBPF}
EOF

args=("$@")

has_no_default_features=0
has_features=0
for arg in "${args[@]}"; do
case "${arg}" in
--no-default-features) has_no_default_features=1 ;;
--features|--all-features) has_features=1 ;;
--features=*) has_features=1 ;;
esac
done

if [[ ${has_no_default_features} -eq 0 ]]; then
args+=(--no-default-features)
fi

if [[ ${ENABLE_ANDROID_EBPF} == 1 && ${has_features} -eq 0 ]]; then
args+=(--features android-ebpf)
fi

restore_lockfile() {
if [[ -n "${lock_backup_path:-}" && -f "${lock_backup_path}" ]]; then
mv "${lock_backup_path}" "${ROOT_DIR}/Cargo.lock"
fi
}

if [[ ${ENABLE_ANDROID_EBPF} == 1 ]]; then
args+=(--config "${ANDROID_EBPF_CONFIG}")

if [[ -f "${ROOT_DIR}/Cargo.lock" ]]; then
lock_backup_path=$(mktemp "${ROOT_DIR}/Cargo.lock.android-ebpf.XXXXXX")
cp "${ROOT_DIR}/Cargo.lock" "${lock_backup_path}"
trap restore_lockfile EXIT
fi
fi

cargo "${args[@]}"
16 changes: 11 additions & 5 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use crate::network::{
// Platform-specific interface stats provider
#[cfg(target_os = "freebsd")]
use crate::network::platform::FreeBSDStatsProvider as PlatformStatsProvider;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "android"))]
use crate::network::platform::LinuxStatsProvider as PlatformStatsProvider;
#[cfg(target_os = "macos")]
use crate::network::platform::MacOSStatsProvider as PlatformStatsProvider;
Expand All @@ -50,6 +50,7 @@ use std::sync::{LazyLock, Mutex};
/// Sandbox status information for UI display
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "windows",
all(target_os = "macos", feature = "macos-sandbox")
))]
Expand All @@ -60,21 +61,22 @@ pub struct SandboxInfo {
/// Whether network connections are blocked
#[cfg(any(
target_os = "linux",
target_os = "android",
all(target_os = "macos", feature = "macos-sandbox")
))]
pub net_restricted: bool,
// Linux-specific fields (Landlock + capabilities)
/// Whether CAP_NET_RAW was dropped
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "android"))]
pub cap_dropped: bool,
/// Whether CAP_BPF/CAP_PERFMON were dropped
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "android"))]
pub ebpf_caps_dropped: bool,
/// Whether Landlock is available on this kernel
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "android"))]
pub landlock_available: bool,
/// Whether Landlock filesystem restrictions are applied
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fs_restricted: bool,
// macOS-specific fields (Seatbelt)
/// Whether Seatbelt sandbox was applied
Expand Down Expand Up @@ -478,6 +480,7 @@ pub struct App {
/// Sandbox status (Linux Landlock / macOS Seatbelt / Windows restricted token)
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "windows",
all(target_os = "macos", feature = "macos-sandbox")
))]
Expand Down Expand Up @@ -584,6 +587,7 @@ impl App {
geoip_resolver,
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "windows",
all(target_os = "macos", feature = "macos-sandbox")
))]
Expand Down Expand Up @@ -1811,6 +1815,7 @@ impl App {
/// Get sandbox status information
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "windows",
all(target_os = "macos", feature = "macos-sandbox")
))]
Expand All @@ -1824,6 +1829,7 @@ impl App {
/// Set sandbox status information
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "windows",
all(target_os = "macos", feature = "macos-sandbox")
))]
Expand Down
4 changes: 2 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use clap::{Arg, Command};

#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "android"))]
const INTERFACE_HELP: &str = "Network interface to monitor (use \"any\" to capture all interfaces)";

#[cfg(not(target_os = "linux"))]
#[cfg(not(any(target_os = "linux", target_os = "android")))]
const INTERFACE_HELP: &str = "Network interface to monitor";

#[cfg(target_os = "macos")]
Expand Down
Loading