diff --git a/.gitignore b/.gitignore index c93f878..21d072e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ debug/ target/ +pcap.pc + +.cargo/ + # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml index 1ed7618..50db6f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ path = "src/main.rs" [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" @@ -45,12 +45,20 @@ 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" + [target.'cfg(windows)'.dependencies] windows = { version = "0.62", features = [ "Win32_Foundation", diff --git a/src/app.rs b/src/app.rs index 6725b5c..c475731 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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; @@ -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") ))] @@ -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 @@ -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") ))] @@ -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") ))] @@ -694,7 +698,7 @@ impl App { *linktype_storage.write().unwrap() = Some(linktype); // Drop CAP_NET_RAW now that the socket is open (Linux only) - #[cfg(all(target_os = "linux", feature = "landlock"))] + #[cfg(all(any(target_os = "linux", target_os = "android"), feature = "landlock"))] { if let Err(e) = crate::network::platform::sandbox::capabilities::drop_cap_net_raw() @@ -920,7 +924,7 @@ impl App { info!("Packet processor {} started", id); // Drop CAP_NET_RAW immediately as this thread doesn't need it (Linux only) - #[cfg(all(target_os = "linux", feature = "landlock"))] + #[cfg(all(any(target_os = "linux", target_os = "android"), feature = "landlock"))] { if let Err(e) = crate::network::platform::sandbox::capabilities::drop_cap_net_raw() @@ -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") ))] @@ -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") ))] diff --git a/src/cli.rs b/src/cli.rs index 9417237..c4e616d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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")] diff --git a/src/main.rs b/src/main.rs index 66dbb8f..330968a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use anyhow::Result; +#[cfg(not(target_os = "android"))] use arboard::Clipboard; use log::{LevelFilter, debug, error, info, warn}; use ratatui::prelude::CrosstermBackend; @@ -518,6 +519,7 @@ fn sort_connections( } /// Copy text to the system clipboard and update UI state with feedback. +#[cfg(not(target_os = "android"))] fn copy_to_clipboard(text: &str, display_msg: &str, ui_state: &mut ui::UIState, app: &app::App) { // Used conditionally on Linux/FreeBSD for sandbox-aware error messages let _ = app; @@ -566,6 +568,16 @@ fn copy_to_clipboard(text: &str, display_msg: &str, ui_state: &mut ui::UIState, } } +/// Android clipboard stub - clipboard not available via arboard on Android +#[cfg(target_os = "android")] +fn copy_to_clipboard(_text: &str, display_msg: &str, ui_state: &mut ui::UIState, _app: &app::App) { + info!("Copy requested (Android): {}", display_msg); + ui_state.clipboard_message = Some(( + format!("Copied (log only): {}", display_msg), + std::time::Instant::now(), + )); +} + fn run_ui_loop( terminal: &mut ui::Terminal, app: &app::App, diff --git a/src/network/capture.rs b/src/network/capture.rs index fbc1d5d..2d7e752 100644 --- a/src/network/capture.rs +++ b/src/network/capture.rs @@ -326,7 +326,7 @@ fn find_capture_device(interface_name: &Option) -> Result { // Special handling for 'any' interface if name == "any" { - #[cfg(not(target_os = "linux"))] + #[cfg(not(any(target_os = "linux", target_os = "android")))] { return Err(anyhow!( "The 'any' interface is only supported on Linux.\n\ @@ -335,7 +335,7 @@ fn find_capture_device(interface_name: &Option) -> Result { )); } - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] { log::info!("Using 'any' pseudo-interface to capture on all interfaces"); } diff --git a/src/network/platform/linux/interface_stats.rs b/src/network/platform/linux/interface_stats.rs index c4aa410..2bb99b6 100644 --- a/src/network/platform/linux/interface_stats.rs +++ b/src/network/platform/linux/interface_stats.rs @@ -67,7 +67,7 @@ fn read_stat(base_path: &str, stat_name: &str) -> Result { } #[cfg(test)] -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] mod tests { use super::*; diff --git a/src/network/platform/mod.rs b/src/network/platform/mod.rs index 009cf86..1611f6c 100644 --- a/src/network/platform/mod.rs +++ b/src/network/platform/mod.rs @@ -18,19 +18,19 @@ pub enum DegradationReason { None, // Linux eBPF reasons /// Missing CAP_BPF capability (Linux 5.8+) - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] MissingCapBpf, /// Missing CAP_PERFMON capability (Linux 5.8+) - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] MissingCapPerfmon, /// Missing both CAP_BPF and CAP_PERFMON (and no CAP_SYS_ADMIN fallback) - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] MissingBpfCapabilities, /// eBPF feature not compiled in - #[cfg(all(target_os = "linux", not(feature = "ebpf")))] + #[cfg(all(any(target_os = "linux", target_os = "android"), not(feature = "ebpf")))] EbpfFeatureDisabled, /// Kernel doesn't support required eBPF features - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] KernelUnsupported, // macOS PKTAP reasons /// No root privileges for PKTAP @@ -52,15 +52,15 @@ impl DegradationReason { pub fn description(&self) -> &str { match self { Self::None => "", - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] Self::MissingCapBpf => "needs CAP_BPF", - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] Self::MissingCapPerfmon => "needs CAP_PERFMON", - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] Self::MissingBpfCapabilities => "needs CAP_BPF+CAP_PERFMON", - #[cfg(all(target_os = "linux", not(feature = "ebpf")))] + #[cfg(all(any(target_os = "linux", target_os = "android"), not(feature = "ebpf")))] Self::EbpfFeatureDisabled => "eBPF feature disabled", - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] Self::KernelUnsupported => "kernel unsupported", #[cfg(target_os = "macos")] Self::MissingRootPrivileges => "needs root", @@ -77,12 +77,12 @@ impl DegradationReason { pub fn unavailable_feature(&self) -> Option<&str> { match self { Self::None => None, - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] Self::MissingCapBpf | Self::MissingCapPerfmon | Self::MissingBpfCapabilities | Self::KernelUnsupported => Some("eBPF"), - #[cfg(all(target_os = "linux", not(feature = "ebpf")))] + #[cfg(all(any(target_os = "linux", target_os = "android"), not(feature = "ebpf")))] Self::EbpfFeatureDisabled => Some("eBPF"), #[cfg(target_os = "macos")] Self::MissingRootPrivileges @@ -96,7 +96,7 @@ impl DegradationReason { // Platform-specific modules (one cfg per platform instead of many) #[cfg(target_os = "freebsd")] mod freebsd; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] mod linux; #[cfg(target_os = "macos")] mod macos; @@ -106,9 +106,9 @@ mod windows; // Re-export factory functions and types from platform modules #[cfg(target_os = "freebsd")] pub use freebsd::{FreeBSDStatsProvider, create_process_lookup}; -#[cfg(all(target_os = "linux", feature = "landlock"))] +#[cfg(all(any(target_os = "linux", target_os = "android"), feature = "landlock"))] pub use linux::sandbox; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] pub use linux::{LinuxStatsProvider, create_process_lookup}; #[cfg(all(target_os = "macos", feature = "macos-sandbox"))] pub use macos::sandbox; diff --git a/src/network/privileges.rs b/src/network/privileges.rs index 9f94579..4a040a1 100644 --- a/src/network/privileges.rs +++ b/src/network/privileges.rs @@ -4,7 +4,7 @@ //! network packets on different platforms (Linux, macOS, Windows). use anyhow::Result; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] use anyhow::anyhow; #[cfg(any( not(any( @@ -42,6 +42,7 @@ impl PrivilegeStatus { /// Create a status indicating insufficient privileges #[cfg(any( target_os = "linux", + target_os = "android", target_os = "macos", target_os = "windows", target_os = "freebsd", @@ -84,7 +85,7 @@ impl PrivilegeStatus { /// Check if the current process has sufficient privileges for packet capture pub fn check_packet_capture_privileges() -> Result { - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] { check_linux_privileges() } @@ -106,6 +107,7 @@ pub fn check_packet_capture_privileges() -> Result { #[cfg(not(any( target_os = "linux", + target_os = "android", target_os = "macos", target_os = "windows", target_os = "freebsd" @@ -117,7 +119,7 @@ pub fn check_packet_capture_privileges() -> Result { } } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] fn check_linux_privileges() -> Result { use std::fs; @@ -181,7 +183,7 @@ fn check_linux_privileges() -> Result { } /// Detect if running inside a container -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] fn is_running_in_container() -> bool { use std::fs; diff --git a/src/ui.rs b/src/ui.rs index 29a09bc..d8602a9 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1974,7 +1974,7 @@ fn draw_stats_panel( // Build the security/sandbox text up front so the chunk height can match // its content. Otherwise long feature lists get clipped on narrow columns. - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] let security_text: Vec = { let sandbox_info = app.get_sandbox_info(); let status_style = match sandbox_info.status.as_str() {