mirror of
https://github.com/domcyrus/rustnet.git
synced 2025-12-30 02:19:50 -06:00
feat: add Landlock sandbox and capability dropping for Linux (#86)
* feat: add Landlock sandbox and capability dropping for Linux - Restrict filesystem access to /proc only after initialization - Block TCP bind/connect on kernel 6.4+ (network sandbox) - Drop CAP_NET_RAW after pcap handle opened - Add --no-sandbox and --sandbox-strict CLI options - Show privilege info on non-Linux platforms in UI - Add SECURITY.md documentation * fix: remove unused set_sandbox_info and hide Landlock line on non-Linux * fix: gate SandboxInfo to Linux only to fix clippy warnings * fix: add is_admin() function for Windows builds The Windows build was failing because ui.rs called crate::is_admin() but the function didn't exist. Added the implementation using Windows Security API to check if the process has elevated privileges. Also added Win32_Security feature to windows crate dependencies. * fix: add is_admin() to main.rs for Windows binary crate The previous fix added is_admin() to lib.rs but ui.rs is compiled as part of the binary crate (main.rs), not the library crate. Added the function to main.rs so crate::is_admin() resolves correctly.
This commit is contained in:
@@ -9,7 +9,7 @@ This document describes the technical architecture and implementation details of
|
||||
- [Platform-Specific Implementations](#platform-specific-implementations)
|
||||
- [Performance Considerations](#performance-considerations)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Security Considerations](#security-considerations)
|
||||
- [Security](#security)
|
||||
|
||||
## Multi-threaded Architecture
|
||||
|
||||
@@ -315,78 +315,6 @@ RustNet is built with the following key dependencies:
|
||||
- **ring** - Cryptographic operations (for TLS/SNI parsing)
|
||||
- **aes** - AES encryption support (for protocol detection)
|
||||
|
||||
## Security Considerations
|
||||
## Security
|
||||
|
||||
### Privileged Access
|
||||
|
||||
RustNet requires privileged access for packet capture:
|
||||
- **Raw socket access** - Intercept network traffic at low level (read-only, non-promiscuous mode)
|
||||
- **BPF device access** - Load packet filters into kernel
|
||||
- **eBPF programs** - Optional kernel probes for enhanced process tracking (Linux only)
|
||||
|
||||
**Mitigation:**
|
||||
- Use Linux capabilities instead of full root (CAP_NET_RAW for packet capture, CAP_BPF+CAP_PERFMON for eBPF)
|
||||
- Use macOS group-based access (`access_bpf` group)
|
||||
- Audit which users have packet capture permissions
|
||||
- Operates in read-only mode - cannot modify or inject packets
|
||||
|
||||
### Read-Only Operation
|
||||
|
||||
The tool only monitors traffic; it does not:
|
||||
- Modify packets
|
||||
- Block connections
|
||||
- Inject traffic
|
||||
- Alter routing tables
|
||||
- Change firewall rules
|
||||
|
||||
### Log File Privacy
|
||||
|
||||
Log files may contain sensitive information:
|
||||
- IP addresses and ports
|
||||
- Hostnames and SNI data
|
||||
- Process names and PIDs
|
||||
- DNS queries and responses
|
||||
|
||||
**Best Practices:**
|
||||
- Disable logging by default (no `--log-level` flag)
|
||||
- Secure log directory permissions
|
||||
- Implement log rotation and retention policies
|
||||
- Review logs for sensitive data before sharing
|
||||
|
||||
### No External Communication
|
||||
|
||||
RustNet operates entirely locally:
|
||||
- No telemetry or analytics
|
||||
- No network requests (except monitored traffic)
|
||||
- No cloud services or remote APIs
|
||||
- All data stays on your system
|
||||
|
||||
### eBPF Security
|
||||
|
||||
When using experimental eBPF support:
|
||||
- Requires additional kernel capabilities (`CAP_BPF`, `CAP_PERFMON`)
|
||||
- eBPF programs are verified by kernel before loading
|
||||
- Limited to read-only operations (no packet modification)
|
||||
- Automatically falls back to procfs if eBPF fails
|
||||
|
||||
### Audit and Compliance
|
||||
|
||||
For production environments:
|
||||
- **Audit logging** of who runs RustNet with packet capture privileges
|
||||
- **Network monitoring policies** and compliance with data protection regulations
|
||||
- **User access reviews** for privileged network access
|
||||
- **Automated capability management** via configuration management systems
|
||||
|
||||
### Threat Model
|
||||
|
||||
**What RustNet protects against:**
|
||||
- Unauthorized users cannot capture packets without proper permissions
|
||||
- Capability-based permissions limit blast radius of compromise
|
||||
|
||||
**What RustNet does NOT protect against:**
|
||||
- Users with packet capture permissions can see all unencrypted traffic
|
||||
- Root/Administrator users can modify RustNet or capture packets directly
|
||||
- Physical access to the machine enables packet capture
|
||||
- Network-level attacks (RustNet is a monitoring tool, not a security appliance)
|
||||
|
||||
For detailed permission setup and security best practices, see [INSTALL.md](INSTALL.md).
|
||||
For security documentation including Landlock sandboxing, privilege requirements, and threat model, see [SECURITY.md](SECURITY.md).
|
||||
|
||||
42
Cargo.lock
generated
42
Cargo.lock
generated
@@ -203,6 +203,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "caps"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo-platform"
|
||||
version = "0.1.9"
|
||||
@@ -699,6 +708,26 @@ version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -1069,6 +1098,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "landlock"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49fefd6652c57d68aaa32544a4c0e642929725bdc1fd929367cdeb673ab81088"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"libc",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -1766,6 +1806,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"bytes",
|
||||
"caps",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
@@ -1775,6 +1816,7 @@ dependencies = [
|
||||
"dashmap",
|
||||
"dns-lookup",
|
||||
"http_req",
|
||||
"landlock",
|
||||
"libbpf-cargo",
|
||||
"libbpf-rs",
|
||||
"libc",
|
||||
|
||||
@@ -48,6 +48,8 @@ procfs = "0.18"
|
||||
libbpf-rs = { version = "0.25", optional = true }
|
||||
bytes = { version = "1.11", optional = true }
|
||||
libc = { version = "0.2", optional = true }
|
||||
landlock = { version = "0.4", optional = true }
|
||||
caps = { version = "0.5", optional = true }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "freebsd"))'.dependencies]
|
||||
libc = "0.2"
|
||||
@@ -58,6 +60,7 @@ windows = { version = "0.62", features = [
|
||||
"Win32_NetworkManagement_IpHelper",
|
||||
"Win32_NetworkManagement_Ndis",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
@@ -80,6 +83,7 @@ windows = { version = "0.62", features = [
|
||||
"Win32_NetworkManagement_IpHelper",
|
||||
"Win32_NetworkManagement_Ndis",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
@@ -91,9 +95,11 @@ libbpf-cargo = { version = "0.25", optional = true }
|
||||
# eBPF is enabled by default for enhanced performance on Linux.
|
||||
# On non-Linux platforms, this feature has no effect as all eBPF code
|
||||
# and dependencies are Linux-specific (guarded by target_os checks).
|
||||
default = ["ebpf"]
|
||||
# Landlock provides security sandboxing on Linux 5.13+.
|
||||
default = ["ebpf", "landlock"]
|
||||
linux-default = ["ebpf"] # Deprecated: kept for backwards compatibility
|
||||
ebpf = ["libbpf-rs", "bytes", "libc", "dep:libbpf-cargo"]
|
||||
landlock = ["dep:landlock", "dep:caps"]
|
||||
|
||||
# Minimal cross configuration to override dependency conflicts
|
||||
[workspace.metadata.cross.build.env]
|
||||
|
||||
@@ -26,6 +26,7 @@ A cross-platform network monitoring tool built with Rust. RustNet provides real-
|
||||
- **Terminal User Interface**: Beautiful TUI built with ratatui with adjustable column widths
|
||||
- **Multi-threaded Processing**: Concurrent packet processing for high performance
|
||||
- **Optional Logging**: Detailed logging with configurable log levels (disabled by default)
|
||||
- **Security Sandboxing**: Landlock-based filesystem/network restrictions on Linux 5.13+ (see [SECURITY.md](SECURITY.md))
|
||||
|
||||
<details>
|
||||
<summary><b>eBPF Enhanced Process Identification (Linux Default)</b></summary>
|
||||
@@ -232,6 +233,7 @@ See [USAGE.md](USAGE.md) for complete timeout details.
|
||||
|
||||
- **[INSTALL.md](INSTALL.md)** - Detailed installation instructions for all platforms, permission setup, and troubleshooting
|
||||
- **[USAGE.md](USAGE.md)** - Complete usage guide including command-line options, filtering, sorting, and logging
|
||||
- **[SECURITY.md](SECURITY.md)** - Security features including Landlock sandboxing and privilege management
|
||||
- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Technical architecture, platform implementations, and performance details
|
||||
- **[PROFILING.md](PROFILING.md)** - Performance profiling guide with flamegraph setup and optimization tips
|
||||
- **[ROADMAP.md](ROADMAP.md)** - Planned features and future improvements
|
||||
|
||||
157
SECURITY.md
Normal file
157
SECURITY.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Security
|
||||
|
||||
RustNet processes untrusted network data, making defense-in-depth security critical. This document describes the security measures implemented.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Landlock Sandboxing (Linux)](#landlock-sandboxing-linux)
|
||||
- [Privilege Requirements](#privilege-requirements)
|
||||
- [Read-Only Operation](#read-only-operation)
|
||||
- [No External Communication](#no-external-communication)
|
||||
- [Log File Privacy](#log-file-privacy)
|
||||
- [eBPF Security](#ebpf-security)
|
||||
- [Threat Model](#threat-model)
|
||||
- [Audit and Compliance](#audit-and-compliance)
|
||||
- [Reporting Security Issues](#reporting-security-issues)
|
||||
|
||||
## Landlock Sandboxing (Linux)
|
||||
|
||||
On Linux 5.13+, RustNet uses [Landlock](https://landlock.io/) to restrict its own capabilities after initialization. This limits the damage if a vulnerability in packet parsing is exploited.
|
||||
|
||||
### What Gets Restricted
|
||||
|
||||
| Restriction | Kernel Version | Description |
|
||||
|-------------|----------------|-------------|
|
||||
| Filesystem | 5.13+ | Only `/proc` readable (for process identification) |
|
||||
| Network | 6.4+ | TCP bind/connect blocked (RustNet is passive) |
|
||||
| Capabilities | Any | `CAP_NET_RAW` dropped after pcap socket opened |
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Initialization phase**: RustNet loads eBPF programs, opens packet capture handles, and creates log files
|
||||
2. **Sandbox application**: After init, Landlock restricts filesystem and network access
|
||||
3. **Capability drop**: `CAP_NET_RAW` is removed from the process (existing pcap socket remains valid)
|
||||
|
||||
### Security Benefits
|
||||
|
||||
If an attacker exploits a vulnerability in DPI/packet parsing:
|
||||
- Cannot read arbitrary files (credentials, configs, etc.)
|
||||
- Cannot write to filesystem (except configured log paths)
|
||||
- Cannot make outbound TCP connections (data exfiltration blocked)
|
||||
- Cannot bind TCP ports (reverse shell blocked)
|
||||
- Cannot create new raw sockets (capability dropped)
|
||||
|
||||
### CLI Options
|
||||
|
||||
```
|
||||
--no-sandbox Disable Landlock sandboxing
|
||||
--sandbox-strict Require full sandbox enforcement or exit
|
||||
```
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
- **Kernel < 5.13**: Sandboxing skipped, warning logged
|
||||
- **Kernel 5.13-6.3**: Filesystem restrictions only
|
||||
- **Kernel 6.4+**: Full filesystem + network restrictions
|
||||
- **Docker**: May be blocked by seccomp; app continues normally
|
||||
|
||||
## Privilege Requirements
|
||||
|
||||
RustNet requires privileged access for packet capture:
|
||||
|
||||
| Platform | Requirement |
|
||||
|----------|-------------|
|
||||
| Linux | `CAP_NET_RAW` capability or root |
|
||||
| macOS | Root or BPF group membership (`access_bpf` group) |
|
||||
| Windows | Administrator (for Npcap) |
|
||||
| FreeBSD | Root or BPF device access |
|
||||
|
||||
### Why Privileges Are Needed
|
||||
|
||||
- **Raw socket access** - Intercept network traffic at low level (read-only, non-promiscuous mode)
|
||||
- **BPF device access** - Load packet filters into kernel
|
||||
- **eBPF programs** - Optional kernel probes for enhanced process tracking (Linux only)
|
||||
|
||||
### Recommended: Capability-based Execution (Linux)
|
||||
|
||||
Instead of running as root, grant only the required capabilities:
|
||||
|
||||
```bash
|
||||
# Modern Linux (5.8+): packet capture + eBPF
|
||||
sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)
|
||||
|
||||
# Legacy Linux (pre-5.8): packet capture + eBPF
|
||||
sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet)
|
||||
|
||||
# Packet capture only (no eBPF process detection)
|
||||
sudo setcap cap_net_raw=eip $(which rustnet)
|
||||
```
|
||||
|
||||
After sandbox application, `CAP_NET_RAW` is dropped - the process retains only the minimum privileges needed.
|
||||
|
||||
## Read-Only Operation
|
||||
|
||||
RustNet only monitors traffic; it does not:
|
||||
- Modify packets
|
||||
- Block connections
|
||||
- Inject traffic
|
||||
- Alter routing tables
|
||||
- Change firewall rules
|
||||
|
||||
The packet capture is opened in non-promiscuous, read-only mode.
|
||||
|
||||
## No External Communication
|
||||
|
||||
RustNet operates entirely locally:
|
||||
- No telemetry or analytics
|
||||
- No network requests (except monitored traffic)
|
||||
- No cloud services or remote APIs
|
||||
- All data stays on your system
|
||||
|
||||
## Log File Privacy
|
||||
|
||||
Log files may contain sensitive information:
|
||||
- IP addresses and ports
|
||||
- Hostnames and SNI data
|
||||
- Process names and PIDs
|
||||
- DNS queries and responses
|
||||
|
||||
**Best Practices:**
|
||||
- Disable logging by default (no `--log-level` flag)
|
||||
- Secure log directory permissions
|
||||
- Implement log rotation and retention policies
|
||||
- Review logs for sensitive data before sharing
|
||||
|
||||
## eBPF Security
|
||||
|
||||
When using eBPF for enhanced process detection (default on Linux):
|
||||
|
||||
- Requires additional kernel capabilities (`CAP_BPF`, `CAP_PERFMON`)
|
||||
- eBPF programs are verified by kernel before loading
|
||||
- Limited to read-only operations (no packet modification)
|
||||
- Automatically falls back to procfs if eBPF fails
|
||||
|
||||
## Threat Model
|
||||
|
||||
**What RustNet protects against:**
|
||||
- Unauthorized users cannot capture packets without proper permissions
|
||||
- Capability-based permissions limit blast radius of compromise
|
||||
- Landlock sandbox contains potential exploitation
|
||||
|
||||
**What RustNet does NOT protect against:**
|
||||
- Users with packet capture permissions can see all unencrypted traffic
|
||||
- Root/Administrator users can modify RustNet or capture packets directly
|
||||
- Physical access to the machine enables packet capture
|
||||
- Network-level attacks (RustNet is a monitoring tool, not a security appliance)
|
||||
|
||||
## Audit and Compliance
|
||||
|
||||
For production environments:
|
||||
- **Audit logging** of who runs RustNet with packet capture privileges
|
||||
- **Network monitoring policies** and compliance with data protection regulations
|
||||
- **User access reviews** for privileged network access
|
||||
- **Automated capability management** via configuration management systems
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
Please report security vulnerabilities via GitHub Issues or contact the maintainers directly.
|
||||
3
USAGE.md
3
USAGE.md
@@ -78,6 +78,9 @@ Options:
|
||||
-r, --refresh-interval <MILLISECONDS> UI refresh interval in milliseconds [default: 1000]
|
||||
--no-dpi Disable deep packet inspection
|
||||
-l, --log-level <LEVEL> Set the log level (if not provided, no logging will be enabled)
|
||||
--json-log <FILE> Enable JSON logging of connection events to specified file
|
||||
--no-sandbox Disable Landlock sandboxing (Linux only)
|
||||
--sandbox-strict Require full sandbox enforcement or exit (Linux only)
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
39
src/app.rs
39
src/app.rs
@@ -36,6 +36,22 @@ use crate::network::platform::WindowsStatsProvider as PlatformStatsProvider;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
/// Sandbox status information for UI display
|
||||
#[cfg(target_os = "linux")]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SandboxInfo {
|
||||
/// Overall status description
|
||||
pub status: String,
|
||||
/// Whether CAP_NET_RAW was dropped
|
||||
pub cap_dropped: bool,
|
||||
/// Whether Landlock is available on this kernel
|
||||
pub landlock_available: bool,
|
||||
/// Whether Landlock filesystem restrictions are applied
|
||||
pub fs_restricted: bool,
|
||||
/// Whether Landlock network restrictions are applied
|
||||
pub net_restricted: bool,
|
||||
}
|
||||
|
||||
/// Global QUIC connection ID to connection key mapping
|
||||
/// This allows tracking QUIC connections across connection ID changes
|
||||
static QUIC_CONNECTION_MAPPING: LazyLock<Mutex<HashMap<String, String>>> =
|
||||
@@ -207,6 +223,10 @@ pub struct App {
|
||||
|
||||
/// Interface rates (per-second rates)
|
||||
interface_rates: Arc<DashMap<String, InterfaceRates>>,
|
||||
|
||||
/// Sandbox status (Linux Landlock)
|
||||
#[cfg(target_os = "linux")]
|
||||
sandbox_info: Arc<RwLock<SandboxInfo>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -231,6 +251,8 @@ impl App {
|
||||
process_detection_method: Arc::new(RwLock::new(String::from("initializing..."))),
|
||||
interface_stats: Arc::new(DashMap::new()),
|
||||
interface_rates: Arc::new(DashMap::new()),
|
||||
#[cfg(target_os = "linux")]
|
||||
sandbox_info: Arc::new(RwLock::new(SandboxInfo::default())),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -999,6 +1021,23 @@ impl App {
|
||||
.unwrap_or_else(|_| String::from("unknown"))
|
||||
}
|
||||
|
||||
/// Get sandbox status information
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn get_sandbox_info(&self) -> SandboxInfo {
|
||||
self.sandbox_info
|
||||
.read()
|
||||
.map(|s| s.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Set sandbox status information
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_sandbox_info(&self, info: SandboxInfo) {
|
||||
if let Ok(mut guard) = self.sandbox_info.write() {
|
||||
*guard = info;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get link layer information for the current interface
|
||||
/// Returns (link_layer_type_name, is_tunnel)
|
||||
pub fn get_link_layer_info(&self) -> (String, bool) {
|
||||
|
||||
13
src/cli.rs
13
src/cli.rs
@@ -56,4 +56,17 @@ pub fn build_cli() -> Command {
|
||||
.help("Enable JSON logging of connection events to specified file")
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-sandbox")
|
||||
.long("no-sandbox")
|
||||
.help("Disable Landlock sandboxing (Linux only)")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("sandbox-strict")
|
||||
.long("sandbox-strict")
|
||||
.help("Require full sandbox enforcement or exit (Linux only)")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
.conflicts_with("no-sandbox"),
|
||||
)
|
||||
}
|
||||
|
||||
38
src/lib.rs
38
src/lib.rs
@@ -7,3 +7,41 @@ pub mod config;
|
||||
pub mod filter;
|
||||
pub mod network;
|
||||
pub mod ui;
|
||||
|
||||
/// Check if the current process is running with Administrator privileges (Windows only)
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_admin() -> bool {
|
||||
use windows::Win32::Foundation::HANDLE;
|
||||
use windows::Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY};
|
||||
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
|
||||
|
||||
unsafe {
|
||||
let mut token_handle = HANDLE::default();
|
||||
|
||||
// Open the process token
|
||||
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut elevation = TOKEN_ELEVATION::default();
|
||||
let mut return_length = 0u32;
|
||||
|
||||
// Get the elevation information
|
||||
let result = GetTokenInformation(
|
||||
token_handle,
|
||||
TokenElevation,
|
||||
Some(&mut elevation as *mut _ as *mut _),
|
||||
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
|
||||
&mut return_length,
|
||||
);
|
||||
|
||||
// Close the token handle
|
||||
let _ = windows::Win32::Foundation::CloseHandle(token_handle);
|
||||
|
||||
if result.is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
elevation.TokenIsElevated != 0
|
||||
}
|
||||
}
|
||||
|
||||
118
src/main.rs
118
src/main.rs
@@ -73,10 +73,88 @@ fn main() -> Result<()> {
|
||||
info!("Terminal UI initialized");
|
||||
|
||||
// Create and start the application
|
||||
let mut app = app::App::new(config)?;
|
||||
let mut app = app::App::new(config.clone())?;
|
||||
app.start()?;
|
||||
info!("Application started");
|
||||
|
||||
// Apply Landlock sandbox (Linux only)
|
||||
// This must be done AFTER app.start() because:
|
||||
// - eBPF programs need to be loaded first (access to /sys/kernel/btf)
|
||||
// - Packet capture handles need to be opened first (access to /dev)
|
||||
// - Log files need to be created first
|
||||
#[cfg(all(target_os = "linux", feature = "landlock"))]
|
||||
{
|
||||
use network::platform::sandbox::{apply_sandbox, SandboxConfig, SandboxMode, SandboxStatus};
|
||||
use std::path::PathBuf;
|
||||
|
||||
let sandbox_mode = if matches.get_flag("no-sandbox") {
|
||||
SandboxMode::Disabled
|
||||
} else if matches.get_flag("sandbox-strict") {
|
||||
SandboxMode::Strict
|
||||
} else {
|
||||
SandboxMode::BestEffort
|
||||
};
|
||||
|
||||
let mut write_paths = Vec::new();
|
||||
|
||||
// Add logs directory if logging is enabled
|
||||
if matches.get_one::<String>("log-level").is_some() {
|
||||
write_paths.push(PathBuf::from("logs"));
|
||||
}
|
||||
|
||||
// Add JSON log path if specified
|
||||
if let Some(json_log_path) = &config.json_log_file {
|
||||
write_paths.push(PathBuf::from(json_log_path));
|
||||
}
|
||||
|
||||
let sandbox_config = SandboxConfig {
|
||||
mode: sandbox_mode,
|
||||
block_network: true, // RustNet is passive, doesn't need TCP
|
||||
write_paths,
|
||||
};
|
||||
|
||||
match apply_sandbox(&sandbox_config) {
|
||||
Ok(result) => {
|
||||
// Update UI with sandbox status
|
||||
let status_str = match result.status {
|
||||
SandboxStatus::FullyEnforced => {
|
||||
info!("Sandbox fully enforced: {}", result.message);
|
||||
"Fully enforced"
|
||||
}
|
||||
SandboxStatus::PartiallyEnforced => {
|
||||
info!("Sandbox partially enforced: {}", result.message);
|
||||
"Partially enforced"
|
||||
}
|
||||
SandboxStatus::NotApplied => {
|
||||
debug!("Sandbox not applied: {}", result.message);
|
||||
"Not applied"
|
||||
}
|
||||
};
|
||||
|
||||
app.set_sandbox_info(app::SandboxInfo {
|
||||
status: status_str.to_string(),
|
||||
cap_dropped: result.cap_net_raw_dropped,
|
||||
landlock_available: result.landlock_available,
|
||||
fs_restricted: result.landlock_fs_applied,
|
||||
net_restricted: result.landlock_net_applied,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
if sandbox_mode == SandboxMode::Strict {
|
||||
return Err(e.context("Sandbox enforcement required but failed"));
|
||||
}
|
||||
info!("Sandbox application error (non-strict mode): {}", e);
|
||||
app.set_sandbox_info(app::SandboxInfo {
|
||||
status: "Error".to_string(),
|
||||
cap_dropped: false,
|
||||
landlock_available: false,
|
||||
fs_restricted: false,
|
||||
net_restricted: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the UI loop
|
||||
let res = run_ui_loop(&mut terminal, &app);
|
||||
|
||||
@@ -654,3 +732,41 @@ fn check_dll_available(dll_name: &str) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the current process is running with Administrator privileges (Windows only)
|
||||
#[cfg(target_os = "windows")]
|
||||
fn is_admin() -> bool {
|
||||
use windows::Win32::Foundation::HANDLE;
|
||||
use windows::Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY};
|
||||
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
|
||||
|
||||
unsafe {
|
||||
let mut token_handle = HANDLE::default();
|
||||
|
||||
// Open the process token
|
||||
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut elevation = TOKEN_ELEVATION::default();
|
||||
let mut return_length = 0u32;
|
||||
|
||||
// Get the elevation information
|
||||
let result = GetTokenInformation(
|
||||
token_handle,
|
||||
TokenElevation,
|
||||
Some(&mut elevation as *mut _ as *mut _),
|
||||
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
|
||||
&mut return_length,
|
||||
);
|
||||
|
||||
// Close the token handle
|
||||
let _ = windows::Win32::Foundation::CloseHandle(token_handle);
|
||||
|
||||
if result.is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
elevation.TokenIsElevated != 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ pub mod ebpf;
|
||||
#[cfg(feature = "ebpf")]
|
||||
mod enhanced;
|
||||
|
||||
#[cfg(feature = "landlock")]
|
||||
pub mod sandbox;
|
||||
|
||||
pub use interface_stats::LinuxStatsProvider;
|
||||
pub use process::LinuxProcessLookup;
|
||||
|
||||
|
||||
85
src/network/platform/linux/sandbox/capabilities.rs
Normal file
85
src/network/platform/linux/sandbox/capabilities.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
//! Linux capability management
|
||||
//!
|
||||
//! Handles dropping capabilities after they are no longer needed.
|
||||
//! This follows the principle of least privilege - capabilities are
|
||||
//! only held while necessary for initialization.
|
||||
//!
|
||||
//! # CAP_NET_RAW
|
||||
//!
|
||||
//! CAP_NET_RAW is required to create raw sockets for packet capture.
|
||||
//! However, once the pcap handle is opened, the capability is no longer
|
||||
//! needed and can be safely dropped. This prevents an attacker from
|
||||
//! creating new raw sockets if they gain code execution.
|
||||
//!
|
||||
//! This is the same pattern used by `ping` and other network utilities.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use caps::{CapSet, Capability};
|
||||
|
||||
/// Drop CAP_NET_RAW from the current process
|
||||
///
|
||||
/// This removes CAP_NET_RAW from both the effective and permitted
|
||||
/// capability sets. The existing pcap socket file descriptor remains
|
||||
/// valid since the capability was only needed to create it.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `Ok(true)` if CAP_NET_RAW was dropped
|
||||
/// - `Ok(false)` if CAP_NET_RAW was not held (nothing to drop)
|
||||
/// - `Err` if dropping failed
|
||||
pub fn drop_cap_net_raw() -> Result<bool> {
|
||||
// Check if we have CAP_NET_RAW in the effective set
|
||||
let has_cap = caps::has_cap(None, CapSet::Effective, Capability::CAP_NET_RAW)
|
||||
.context("Failed to check CAP_NET_RAW in effective set")?;
|
||||
|
||||
if !has_cap {
|
||||
log::debug!("CAP_NET_RAW not in effective set, nothing to drop");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Drop from effective set first
|
||||
caps::drop(None, CapSet::Effective, Capability::CAP_NET_RAW)
|
||||
.context("Failed to drop CAP_NET_RAW from effective set")?;
|
||||
|
||||
log::debug!("Dropped CAP_NET_RAW from effective set");
|
||||
|
||||
// Also drop from permitted set to prevent re-acquiring
|
||||
// This is optional but provides stronger security
|
||||
if caps::has_cap(None, CapSet::Permitted, Capability::CAP_NET_RAW)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Err(e) = caps::drop(None, CapSet::Permitted, Capability::CAP_NET_RAW) {
|
||||
// Not fatal - we already dropped from effective
|
||||
log::warn!("Could not drop CAP_NET_RAW from permitted set: {}", e);
|
||||
} else {
|
||||
log::debug!("Dropped CAP_NET_RAW from permitted set");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Check if CAP_NET_RAW is currently held in the effective set
|
||||
pub fn has_cap_net_raw() -> bool {
|
||||
caps::has_cap(None, CapSet::Effective, Capability::CAP_NET_RAW).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_has_cap_net_raw_does_not_panic() {
|
||||
// This should not panic regardless of capabilities
|
||||
let _ = has_cap_net_raw();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drop_cap_net_raw_without_capability() {
|
||||
// If we don't have CAP_NET_RAW, drop should return Ok(false)
|
||||
// This test may behave differently depending on test environment
|
||||
let result = drop_cap_net_raw();
|
||||
// Should not error, just return whether it was dropped
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
209
src/network/platform/linux/sandbox/landlock.rs
Normal file
209
src/network/platform/linux/sandbox/landlock.rs
Normal file
@@ -0,0 +1,209 @@
|
||||
//! Landlock sandboxing implementation
|
||||
//!
|
||||
//! Landlock is a Linux Security Module (LSM) that allows unprivileged
|
||||
//! processes to restrict their own ambient rights (filesystem access,
|
||||
//! network access).
|
||||
//!
|
||||
//! # Kernel Requirements
|
||||
//!
|
||||
//! - Linux 5.13+: Filesystem access control (ABI v1)
|
||||
//! - Linux 5.19+: File referring/REFER (ABI v2)
|
||||
//! - Linux 6.2+: Truncate control (ABI v3)
|
||||
//! - Linux 6.4+: Network TCP bind/connect (ABI v4)
|
||||
//!
|
||||
//! # What We Restrict
|
||||
//!
|
||||
//! - Filesystem: Only allow read access to `/proc` (needed for process lookup)
|
||||
//! - Filesystem: Only allow write access to specified paths (logs)
|
||||
//! - Network: Block TCP bind and connect (RustNet is passive)
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use landlock::{
|
||||
Access, AccessFs, AccessNet, BitFlags, LandlockStatus, PathBeneath, PathFd, Ruleset,
|
||||
RulesetAttr, RulesetCreatedAttr, RulesetStatus, ABI,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
use super::{SandboxConfig, SandboxMode};
|
||||
|
||||
/// Result of Landlock application
|
||||
pub struct LandlockResult {
|
||||
/// Whether filesystem restrictions were applied
|
||||
pub fs_applied: bool,
|
||||
/// Whether network restrictions were applied
|
||||
pub net_applied: bool,
|
||||
/// Human-readable message
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Check if Landlock is available by attempting a test restriction
|
||||
/// Note: This actually attempts to create a minimal ruleset to check
|
||||
pub fn is_available() -> bool {
|
||||
// Try to create a minimal ruleset - this will fail if Landlock isn't available
|
||||
Ruleset::default()
|
||||
.handle_access(AccessFs::Execute)
|
||||
.and_then(|r| r.create())
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Apply Landlock restrictions based on configuration
|
||||
pub fn apply_landlock(config: &SandboxConfig) -> Result<LandlockResult> {
|
||||
// Check if disabled
|
||||
if config.mode == SandboxMode::Disabled {
|
||||
return Ok(LandlockResult {
|
||||
fs_applied: false,
|
||||
net_applied: false,
|
||||
message: "Sandbox disabled".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Use ABI V4 which includes network support
|
||||
// The crate will automatically handle compatibility with older kernels
|
||||
let abi = ABI::V4;
|
||||
|
||||
// Build filesystem access rights for reading
|
||||
// We need read access to /proc for process identification
|
||||
let read_access = AccessFs::from_read(abi);
|
||||
|
||||
// Build filesystem access rights for writing
|
||||
let write_access = AccessFs::from_all(abi);
|
||||
|
||||
// Start building the ruleset
|
||||
let ruleset = Ruleset::default()
|
||||
.handle_access(AccessFs::from_all(abi))
|
||||
.context("Failed to handle filesystem access")?;
|
||||
|
||||
// Add network handling if requested
|
||||
// This will be ignored on kernels that don't support it (< 6.4)
|
||||
let ruleset = if config.block_network {
|
||||
// Try to handle network access - ignore errors for older kernels
|
||||
match ruleset
|
||||
.handle_access(AccessNet::BindTcp)
|
||||
.and_then(|r| r.handle_access(AccessNet::ConnectTcp))
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
// Network access handling failed, recreate without network
|
||||
log::debug!("Network access handling not supported, continuing without");
|
||||
Ruleset::default()
|
||||
.handle_access(AccessFs::from_all(abi))
|
||||
.context("Failed to handle filesystem access")?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ruleset
|
||||
};
|
||||
|
||||
// Create the ruleset
|
||||
let mut ruleset_created = ruleset.create().context("Failed to create Landlock ruleset")?;
|
||||
|
||||
// Add rule for /proc (read-only)
|
||||
// This is required for process identification via procfs
|
||||
if let Err(e) = add_path_rule(&mut ruleset_created, "/proc", read_access) {
|
||||
log::warn!("Could not add /proc rule: {}", e);
|
||||
}
|
||||
|
||||
// Add rules for write paths (logs, etc.)
|
||||
for path in &config.write_paths {
|
||||
if path.exists() {
|
||||
if let Err(e) = add_path_rule(&mut ruleset_created, path, write_access) {
|
||||
log::warn!("Could not add write rule for {:?}: {}", path, e);
|
||||
}
|
||||
} else {
|
||||
// For paths that don't exist yet, try to add rule for parent directory
|
||||
if let Some(parent) = path.parent()
|
||||
&& parent.exists()
|
||||
&& let Err(e) = add_path_rule(&mut ruleset_created, parent, write_access)
|
||||
{
|
||||
log::warn!("Could not add write rule for parent {:?}: {}", parent, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If network blocking is enabled, we DON'T add any NetPort rules
|
||||
// This means all TCP bind/connect operations are blocked by default
|
||||
// (We're handling the access types but not allowing any ports)
|
||||
|
||||
// Apply the restrictions
|
||||
let status = ruleset_created
|
||||
.restrict_self()
|
||||
.context("Failed to apply Landlock restrictions")?;
|
||||
|
||||
// Determine what was actually applied based on the returned status
|
||||
let fs_applied = matches!(
|
||||
status.ruleset,
|
||||
RulesetStatus::FullyEnforced | RulesetStatus::PartiallyEnforced
|
||||
);
|
||||
|
||||
// Check if network restrictions were actually applied
|
||||
let net_applied = config.block_network
|
||||
&& fs_applied
|
||||
&& matches!(
|
||||
status.landlock,
|
||||
LandlockStatus::Available { effective_abi, .. } if effective_abi >= ABI::V4
|
||||
);
|
||||
|
||||
let message = match (&status.ruleset, &status.landlock) {
|
||||
(RulesetStatus::FullyEnforced, _) => "Landlock fully enforced".to_string(),
|
||||
(RulesetStatus::PartiallyEnforced, _) => "Landlock partially enforced".to_string(),
|
||||
(RulesetStatus::NotEnforced, LandlockStatus::NotEnabled) => {
|
||||
"Landlock disabled in kernel".to_string()
|
||||
}
|
||||
(RulesetStatus::NotEnforced, LandlockStatus::NotImplemented) => {
|
||||
"Landlock not implemented in kernel".to_string()
|
||||
}
|
||||
(RulesetStatus::NotEnforced, _) => "Landlock not enforced".to_string(),
|
||||
};
|
||||
|
||||
log::info!("Landlock: {}", message);
|
||||
log::info!(
|
||||
"Landlock: filesystem={}, network={}, landlock_status={:?}",
|
||||
fs_applied,
|
||||
net_applied,
|
||||
status.landlock
|
||||
);
|
||||
|
||||
Ok(LandlockResult {
|
||||
fs_applied,
|
||||
net_applied,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a rule for a path with the specified access rights
|
||||
fn add_path_rule(
|
||||
ruleset: &mut landlock::RulesetCreated,
|
||||
path: impl AsRef<Path>,
|
||||
access: BitFlags<AccessFs>,
|
||||
) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
let fd = PathFd::new(path).with_context(|| format!("Failed to open {:?}", path))?;
|
||||
ruleset
|
||||
.add_rule(PathBeneath::new(fd, access))
|
||||
.with_context(|| format!("Failed to add rule for {:?}", path))?;
|
||||
log::debug!("Landlock: Added rule for {:?}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_available_does_not_panic() {
|
||||
// Should not panic regardless of kernel support
|
||||
let _ = is_available();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_mode() {
|
||||
let config = SandboxConfig {
|
||||
mode: SandboxMode::Disabled,
|
||||
block_network: true,
|
||||
write_paths: vec![],
|
||||
};
|
||||
let result = apply_landlock(&config).unwrap();
|
||||
assert!(!result.fs_applied);
|
||||
assert!(!result.net_applied);
|
||||
}
|
||||
}
|
||||
216
src/network/platform/linux/sandbox/mod.rs
Normal file
216
src/network/platform/linux/sandbox/mod.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
//! Linux sandboxing support
|
||||
//!
|
||||
//! Provides Landlock-based sandboxing for restricting process capabilities
|
||||
//! after initialization is complete. This is a defense-in-depth measure
|
||||
//! that limits damage if the application (processing untrusted network data)
|
||||
//! is compromised.
|
||||
//!
|
||||
//! # Security Model
|
||||
//!
|
||||
//! After sandboxing is applied:
|
||||
//! - Filesystem: Only `/proc` readable, specified write paths writable
|
||||
//! - Network: TCP bind/connect blocked (kernel 6.4+)
|
||||
//! - Capabilities: CAP_NET_RAW dropped (cannot create new raw sockets)
|
||||
//!
|
||||
//! # Compatibility
|
||||
//!
|
||||
//! - Kernel 5.13+: Filesystem sandboxing
|
||||
//! - Kernel 6.4+: Network sandboxing (TCP bind/connect)
|
||||
//! - Older kernels: Graceful degradation (sandbox not applied)
|
||||
|
||||
#[cfg(feature = "landlock")]
|
||||
mod capabilities;
|
||||
#[cfg(feature = "landlock")]
|
||||
mod landlock;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Sandbox enforcement mode
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum SandboxMode {
|
||||
/// Apply sandbox with best-effort (graceful degradation on older kernels)
|
||||
#[default]
|
||||
BestEffort,
|
||||
/// Require full sandbox enforcement or fail
|
||||
Strict,
|
||||
/// Disable sandboxing entirely
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// Configuration for the sandbox
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SandboxConfig {
|
||||
/// Sandbox enforcement mode
|
||||
pub mode: SandboxMode,
|
||||
/// Block TCP bind/connect (recommended for passive monitors)
|
||||
pub block_network: bool,
|
||||
/// Paths that need write access (e.g., log files)
|
||||
pub write_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Result of sandbox application
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SandboxResult {
|
||||
/// Overall status
|
||||
pub status: SandboxStatus,
|
||||
/// Human-readable message
|
||||
pub message: String,
|
||||
/// Whether CAP_NET_RAW was dropped
|
||||
pub cap_net_raw_dropped: bool,
|
||||
/// Whether Landlock is available on this kernel
|
||||
pub landlock_available: bool,
|
||||
/// Whether Landlock filesystem restrictions were applied
|
||||
pub landlock_fs_applied: bool,
|
||||
/// Whether Landlock network restrictions were applied
|
||||
pub landlock_net_applied: bool,
|
||||
}
|
||||
|
||||
/// Status of sandbox application
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SandboxStatus {
|
||||
/// Sandbox fully enforced (all requested restrictions applied)
|
||||
FullyEnforced,
|
||||
/// Sandbox partially enforced (some features unavailable on this kernel)
|
||||
PartiallyEnforced,
|
||||
/// Sandbox not applied (disabled, or kernel doesn't support)
|
||||
NotApplied,
|
||||
}
|
||||
|
||||
/// Apply the sandbox with the given configuration
|
||||
///
|
||||
/// This should be called AFTER:
|
||||
/// - eBPF programs are loaded
|
||||
/// - Packet capture handles are opened
|
||||
/// - Log files are created
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(SandboxResult)` with details about what was applied.
|
||||
/// In `Strict` mode, returns `Err` if full sandboxing cannot be achieved.
|
||||
#[cfg(feature = "landlock")]
|
||||
pub fn apply_sandbox(config: &SandboxConfig) -> anyhow::Result<SandboxResult> {
|
||||
use anyhow::Context;
|
||||
|
||||
// Check Landlock availability upfront
|
||||
let landlock_available = landlock::is_available();
|
||||
|
||||
// Handle disabled mode
|
||||
if config.mode == SandboxMode::Disabled {
|
||||
log::info!("Sandbox disabled by configuration");
|
||||
return Ok(SandboxResult {
|
||||
status: SandboxStatus::NotApplied,
|
||||
message: "Sandbox disabled by configuration".to_string(),
|
||||
cap_net_raw_dropped: false,
|
||||
landlock_available,
|
||||
landlock_fs_applied: false,
|
||||
landlock_net_applied: false,
|
||||
});
|
||||
}
|
||||
|
||||
let mut result = SandboxResult {
|
||||
status: SandboxStatus::FullyEnforced,
|
||||
message: String::new(),
|
||||
cap_net_raw_dropped: false,
|
||||
landlock_available,
|
||||
landlock_fs_applied: false,
|
||||
landlock_net_applied: false,
|
||||
};
|
||||
|
||||
let mut messages = Vec::new();
|
||||
|
||||
// Step 1: Drop CAP_NET_RAW capability
|
||||
// This prevents creating new raw sockets for exfiltration
|
||||
match capabilities::drop_cap_net_raw() {
|
||||
Ok(dropped) => {
|
||||
if dropped {
|
||||
// Verify the drop actually worked
|
||||
if capabilities::has_cap_net_raw() {
|
||||
log::error!("CAP_NET_RAW drop reported success but capability still present!");
|
||||
result.cap_net_raw_dropped = false;
|
||||
messages.push("CAP_NET_RAW drop verification failed".to_string());
|
||||
} else {
|
||||
result.cap_net_raw_dropped = true;
|
||||
messages.push("CAP_NET_RAW dropped".to_string());
|
||||
log::info!("Dropped CAP_NET_RAW capability (verified)");
|
||||
}
|
||||
} else {
|
||||
messages.push("CAP_NET_RAW was not held".to_string());
|
||||
log::debug!("CAP_NET_RAW was not in effective set");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("Failed to drop CAP_NET_RAW: {}", e);
|
||||
log::warn!("{}", msg);
|
||||
messages.push(msg);
|
||||
if config.mode == SandboxMode::Strict {
|
||||
return Err(e).context("Strict mode requires CAP_NET_RAW to be droppable");
|
||||
}
|
||||
result.status = SandboxStatus::PartiallyEnforced;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Apply Landlock restrictions
|
||||
match landlock::apply_landlock(config) {
|
||||
Ok(ll_result) => {
|
||||
result.landlock_fs_applied = ll_result.fs_applied;
|
||||
result.landlock_net_applied = ll_result.net_applied;
|
||||
|
||||
if ll_result.fs_applied {
|
||||
messages.push("Landlock filesystem restrictions applied".to_string());
|
||||
}
|
||||
if ll_result.net_applied {
|
||||
messages.push("Landlock network restrictions applied".to_string());
|
||||
}
|
||||
if !ll_result.fs_applied && !ll_result.net_applied {
|
||||
messages.push(format!("Landlock not applied: {}", ll_result.message));
|
||||
if config.mode == SandboxMode::Strict {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Strict mode requires Landlock support: {}",
|
||||
ll_result.message
|
||||
));
|
||||
}
|
||||
if result.status == SandboxStatus::FullyEnforced {
|
||||
result.status = SandboxStatus::PartiallyEnforced;
|
||||
}
|
||||
} else if !ll_result.net_applied && config.block_network {
|
||||
// Filesystem applied but network not available
|
||||
if result.status == SandboxStatus::FullyEnforced {
|
||||
result.status = SandboxStatus::PartiallyEnforced;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("Landlock application failed: {}", e);
|
||||
log::warn!("{}", msg);
|
||||
messages.push(msg);
|
||||
if config.mode == SandboxMode::Strict {
|
||||
return Err(e).context("Strict mode requires Landlock");
|
||||
}
|
||||
result.status = SandboxStatus::PartiallyEnforced;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine final status
|
||||
if !result.cap_net_raw_dropped && !result.landlock_fs_applied && !result.landlock_net_applied {
|
||||
result.status = SandboxStatus::NotApplied;
|
||||
}
|
||||
|
||||
result.message = messages.join("; ");
|
||||
log::info!("Sandbox result: {:?} - {}", result.status, result.message);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Stub implementation when Landlock feature is disabled
|
||||
#[cfg(not(feature = "landlock"))]
|
||||
pub fn apply_sandbox(_config: &SandboxConfig) -> anyhow::Result<SandboxResult> {
|
||||
log::debug!("Landlock feature not compiled in");
|
||||
Ok(SandboxResult {
|
||||
status: SandboxStatus::NotApplied,
|
||||
message: "Landlock feature not compiled in".to_string(),
|
||||
cap_net_raw_dropped: false,
|
||||
landlock_available: false,
|
||||
landlock_fs_applied: false,
|
||||
landlock_net_applied: false,
|
||||
})
|
||||
}
|
||||
@@ -24,6 +24,8 @@ mod windows;
|
||||
pub use freebsd::{FreeBSDProcessLookup, FreeBSDStatsProvider, create_process_lookup};
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::{LinuxStatsProvider, create_process_lookup};
|
||||
#[cfg(all(target_os = "linux", feature = "landlock"))]
|
||||
pub use linux::sandbox;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::{MacOSStatsProvider, create_process_lookup};
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
89
src/ui.rs
89
src/ui.rs
@@ -710,6 +710,7 @@ fn draw_stats_panel(
|
||||
Constraint::Length(10), // Connection stats (increased for interface line)
|
||||
Constraint::Length(5), // Traffic stats
|
||||
Constraint::Length(7), // Network stats (TCP analytics + header)
|
||||
Constraint::Length(4), // Security stats (sandbox)
|
||||
Constraint::Min(0), // Interface stats
|
||||
])
|
||||
.split(area);
|
||||
@@ -845,6 +846,90 @@ fn draw_stats_panel(
|
||||
.style(Style::default());
|
||||
f.render_widget(network_stats, chunks[2]);
|
||||
|
||||
// Security statistics (sandbox) - Linux only shows Landlock info
|
||||
#[cfg(target_os = "linux")]
|
||||
let security_text: Vec<Line> = {
|
||||
let sandbox_info = app.get_sandbox_info();
|
||||
let status_style = match sandbox_info.status.as_str() {
|
||||
"Fully enforced" => Style::default().fg(Color::Green),
|
||||
"Partially enforced" => Style::default().fg(Color::Yellow),
|
||||
"Not applied" | "Error" => Style::default().fg(Color::Red),
|
||||
_ => Style::default(),
|
||||
};
|
||||
|
||||
let mut features = Vec::new();
|
||||
if sandbox_info.cap_dropped {
|
||||
features.push("CAP_NET_RAW dropped");
|
||||
}
|
||||
if sandbox_info.fs_restricted {
|
||||
features.push("FS restricted");
|
||||
}
|
||||
if sandbox_info.net_restricted {
|
||||
features.push("Net blocked");
|
||||
}
|
||||
|
||||
let available_indicator = if sandbox_info.landlock_available {
|
||||
Span::styled(" [kernel supported]", Style::default().fg(Color::DarkGray))
|
||||
} else {
|
||||
Span::styled(" [kernel unsupported]", Style::default().fg(Color::DarkGray))
|
||||
};
|
||||
|
||||
vec![
|
||||
Line::from(vec![
|
||||
Span::raw("Landlock: "),
|
||||
Span::styled(sandbox_info.status.clone(), status_style),
|
||||
available_indicator,
|
||||
]),
|
||||
Line::from(Span::styled(
|
||||
if features.is_empty() {
|
||||
"No restrictions active".to_string()
|
||||
} else {
|
||||
features.join(", ")
|
||||
},
|
||||
Style::default().fg(Color::Gray),
|
||||
)),
|
||||
]
|
||||
};
|
||||
|
||||
// Non-Linux platforms: show privilege info without mentioning Landlock
|
||||
#[cfg(all(unix, not(target_os = "linux")))]
|
||||
let security_text: Vec<Line> = {
|
||||
let uid = unsafe { libc::geteuid() };
|
||||
let is_root = uid == 0;
|
||||
if is_root {
|
||||
vec![Line::from(Span::styled(
|
||||
"Running as root (UID 0)",
|
||||
Style::default().fg(Color::Yellow),
|
||||
))]
|
||||
} else {
|
||||
vec![Line::from(Span::styled(
|
||||
format!("Running as UID {}", uid),
|
||||
Style::default().fg(Color::Green),
|
||||
))]
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let security_text: Vec<Line> = {
|
||||
let is_elevated = crate::is_admin();
|
||||
if is_elevated {
|
||||
vec![Line::from(Span::styled(
|
||||
"Running as Administrator",
|
||||
Style::default().fg(Color::Yellow),
|
||||
))]
|
||||
} else {
|
||||
vec![Line::from(Span::styled(
|
||||
"Running as standard user",
|
||||
Style::default().fg(Color::Green),
|
||||
))]
|
||||
}
|
||||
};
|
||||
|
||||
let security_stats = Paragraph::new(security_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("Security"))
|
||||
.style(Style::default());
|
||||
f.render_widget(security_stats, chunks[3]);
|
||||
|
||||
// Interface statistics
|
||||
let all_interface_stats = app.get_interface_stats();
|
||||
let interface_rates = app.get_interface_rates();
|
||||
@@ -885,7 +970,7 @@ fn draw_stats_panel(
|
||||
// Calculate how many interfaces can fit in the available space
|
||||
// Each interface takes 2 lines, and we need 2 lines for borders
|
||||
// Reserve 1 line for the "... N more" message if needed
|
||||
let available_height = chunks[3].height as usize;
|
||||
let available_height = chunks[4].height as usize;
|
||||
let lines_for_borders = 2;
|
||||
let lines_per_interface = 2;
|
||||
let lines_for_more_message = 1;
|
||||
@@ -967,7 +1052,7 @@ fn draw_stats_panel(
|
||||
.title("Interface Stats (press 'i')"),
|
||||
)
|
||||
.style(Style::default());
|
||||
f.render_widget(interface_stats_widget, chunks[3]);
|
||||
f.render_widget(interface_stats_widget, chunks[4]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user