Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
416a90d
ci: run all tests on macOS and Linux; drop separate weak job; keep su…
blink-so[bot] Sep 12, 2025
95be2e2
fix(test): make --test method parsing case-insensitive reliably
blink-so[bot] Sep 12, 2025
6361663
ci: exclude linux_integration from non-root run; run it under sudo only
blink-so[bot] Sep 12, 2025
632fdcd
ci: correct nextest filter to exclude linux_integration from non-root…
blink-so[bot] Sep 12, 2025
398fafb
ci: fix nextest filter syntax to '!(binary == "linux_integration")'
blink-so[bot] Sep 12, 2025
8dcced4
ci: use nextest function predicate not(binary("linux_integration")) f…
blink-so[bot] Sep 12, 2025
a621d3a
ci: fix nextest filter syntax to 'not binary(linux_integration)'
blink-so[bot] Sep 12, 2025
9eb7c72
ci: exclude weak_integration from Linux non-root sweep; run only on m…
blink-so[bot] Sep 12, 2025
6887b21
ci: run weak_integration on Linux (install curl, pre-build binary)
blink-so[bot] Sep 12, 2025
f45b1fe
ci: align CARGO_TARGET_DIR with tests (use workspace target for weak_…
blink-so[bot] Sep 12, 2025
2e7a656
ci: harden weak_integration on Linux (install libstdc++; persist CARG…
blink-so[bot] Sep 12, 2025
c2b094f
fix: move canary files from /tmp to user data dir (or ~/.httpjail_can…
blink-so[bot] Sep 12, 2025
e43f377
fix: repair broken merge in managed.rs and add missing imports
blink-so[bot] Sep 12, 2025
149a102
fmt: apply cargo fmt to fix CI
blink-so[bot] Sep 12, 2025
4276141
fix: restore ManagedJail lifecycle fields and simplify impl signature
blink-so[bot] Sep 12, 2025
3ec9c07
fix: compilation errors and clippy warnings
blink-so[bot] Sep 12, 2025
a132124
fmt: apply cargo fmt
blink-so[bot] Sep 12, 2025
87cf0d1
fix: don't use ManagedJail for WeakJail
blink-so[bot] Sep 12, 2025
2b0500d
fmt: apply cargo fmt
blink-so[bot] Sep 12, 2025
1f770d7
fix: unused import warning on macOS and formatting
blink-so[bot] Sep 12, 2025
87c6b6d
ci: optimize weak tests by reusing target directory
blink-so[bot] Sep 12, 2025
baab876
fix: correct weak_integration test command syntax
blink-so[bot] Sep 12, 2025
f1d0f27
fix: build httpjail binary before running weak tests
blink-so[bot] Sep 12, 2025
6adde00
fix: improve httpjail binary handling in tests
blink-so[bot] Sep 12, 2025
c60b3e0
simplify: always build fresh httpjail binary in tests
blink-so[bot] Sep 12, 2025
c60cb25
perf: build httpjail binary only once per test process
blink-so[bot] Sep 12, 2025
8c3f2e4
fix: use OnceLock instead of unsafe static mut
blink-so[bot] Sep 12, 2025
b301ee1
fix: build httpjail binary before running weak tests
blink-so[bot] Sep 12, 2025
875a3cb
improve: error messages for httpjail binary not found
blink-so[bot] Sep 12, 2025
ddc8b7d
fix: move test_https_allow to module level
blink-so[bot] Sep 12, 2025
fa7286f
improve: add diagnostics for binary build verification
blink-so[bot] Sep 12, 2025
63cba5a
refactor: build httpjail binary once at the start of CI
blink-so[bot] Sep 12, 2025
87a20b1
fix: handle CARGO_TARGET_DIR and export binary path
blink-so[bot] Sep 12, 2025
d9da12b
improve: better diagnostics for binary location in CI
blink-so[bot] Sep 12, 2025
eab6360
fix: use --target-dir to control where cargo builds
blink-so[bot] Sep 12, 2025
1a566be
fix: resolve clippy warning about collapsible if
blink-so[bot] Sep 12, 2025
ea530dc
fmt: add trailing newline
blink-so[bot] Sep 12, 2025
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
100 changes: 30 additions & 70 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,18 @@ jobs:
shared-key: ${{ runner.os }}

- name: Install nextest
uses: taiki-e/install-action@nextest
run: cargo install cargo-nextest --locked

- name: Build
run: cargo build --verbose

- name: Run unit tests
run: cargo nextest run --profile ci --lib --verbose

- name: Run smoke tests
run: cargo nextest run --profile ci --test smoke_test --verbose

- name: Run script integration tests
run: cargo nextest run --profile ci --test script_integration --verbose

- name: Run weak mode integration tests
- name: Build httpjail binary
run: |
# On macOS, we only support weak mode due to PF limitations
# (PF translation rules cannot match on user/group)
cargo nextest run --profile ci --test weak_integration --verbose
cargo build --bin httpjail --target-dir target
export HTTPJAIL_BIN="$(pwd)/target/debug/httpjail"
echo "Binary built at: ${HTTPJAIL_BIN}"
ls -la "${HTTPJAIL_BIN}"
echo "HTTPJAIL_BIN=${HTTPJAIL_BIN}" >> $GITHUB_ENV

- name: Run all tests
run: cargo nextest run --profile ci

test-linux:
name: Linux Tests
Expand Down Expand Up @@ -92,76 +85,43 @@ jobs:
- name: Setup Rust environment and install nextest
run: |
source ~/.cargo/env
rustup default stable
cargo install cargo-nextest || true

# Install nextest if not already present
if ! command -v cargo-nextest &> /dev/null; then
cargo install cargo-nextest --locked
- name: Fix target directory permissions from previous runs
run: |
if [ -d target ]; then
sudo chown -R ci:ci target || true
fi

- name: Build
- name: Build httpjail binary
run: |
source ~/.cargo/env
# Use incremental compilation for faster builds
export CARGO_INCREMENTAL=1
cargo build --verbose
cargo build --bin httpjail --target-dir target
export HTTPJAIL_BIN="$(pwd)/target/debug/httpjail"
echo "Binary built at: ${HTTPJAIL_BIN}"
ls -la "${HTTPJAIL_BIN}"
echo "HTTPJAIL_BIN=${HTTPJAIL_BIN}" >> $GITHUB_ENV

- name: Run unit tests
- name: Run all tests (non-root)
run: |
source ~/.cargo/env
cargo nextest run --profile ci --lib --verbose
cargo nextest run --profile ci --verbose -E 'not (binary(linux_integration) or binary(weak_integration))'

- name: Run smoke tests
run: |
source ~/.cargo/env
cargo nextest run --profile ci --test smoke_test --verbose
- name: Install dependencies for weak mode (curl)
run: sudo apt-get update && sudo apt-get install -y curl

- name: Run script integration tests
- name: Run weak mode integration tests (Linux)
run: |
source ~/.cargo/env
cargo nextest run --profile ci --test script_integration --verbose
cargo nextest run --profile ci --test weak_integration

- name: Run Linux jail integration tests
- name: Run Linux jail integration tests (sudo)
run: |
source ~/.cargo/env
# Run all tests without CI workarounds since this is a self-hosted runner
# Run Linux-specific jail tests with sudo to satisfy root requirements
sudo -E $(which cargo) nextest run --profile ci --test linux_integration --verbose

- name: Run isolated cleanup tests
run: |
source ~/.cargo/env
# Run only the comprehensive cleanup and sigint tests with the feature flag
# These tests need to run in isolation from other tests
sudo -E $(which cargo) test --test linux_integration --features isolated-cleanup-tests -- test_comprehensive_resource_cleanup test_cleanup_after_sigint

test-weak:
name: Weak Mode Integration Tests (Linux)
runs-on: ubuntu-latest-8-cores

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable

- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: ${{ runner.os }}

- name: Install nextest
uses: taiki-e/install-action@nextest

- name: Build
run: cargo build --verbose

- name: Run script integration tests
run: cargo nextest run --profile ci --test script_integration --verbose

- name: Run weak mode integration tests
run: cargo nextest run --profile ci --test weak_integration --verbose

clippy:
name: Clippy (${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down
22 changes: 12 additions & 10 deletions src/jail/linux/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
mod nftables;
mod resources;

use super::{Jail, JailConfig};
use super::Jail;
use super::JailConfig;
use crate::sys_resource::ManagedResource;
use anyhow::{Context, Result};
use resources::{NFTable, NamespaceConfig, NetworkNamespace, VethPair};
Expand Down Expand Up @@ -492,15 +493,16 @@ nameserver 8.8.4.4\n",
namespace_name
);

// Create a temporary resolv.conf with public DNS
let temp_resolv = format!("/tmp/httpjail_resolv_{}.conf", &namespace_name);
std::fs::write(
&temp_resolv,
"# Temporary DNS for httpjail namespace\n\
nameserver 8.8.8.8\n\
nameserver 8.8.4.4\n\
nameserver 1.1.1.1\n",
)?;
// Setup DNS for the namespace
// Create a temporary resolv.conf before running the nsenter command
let temp_dir = crate::jail::get_temp_dir();
std::fs::create_dir_all(&temp_dir).ok();
let temp_resolv = temp_dir
.join(format!("httpjail_resolv_{}.conf", &namespace_name))
.to_string_lossy()
.to_string();
std::fs::write(&temp_resolv, "nameserver 1.1.1.1\nnameserver 8.8.8.8\n")
.with_context(|| format!("Failed to create temp resolv.conf: {}", temp_resolv))?;

// First, try to directly write to /etc/resolv.conf in the namespace using echo
let write_cmd = Command::new("ip")
Expand Down
9 changes: 5 additions & 4 deletions src/jail/managed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ use std::thread::{self, JoinHandle};
use std::time::{Duration, SystemTime};
use tracing::{debug, error, info, warn};

/// A jail with lifecycle management (heartbeat and orphan cleanup)
use crate::jail::get_canary_dir;

/// Manages jail lifecycle and cleanup with automatic cleanup on drop
pub struct ManagedJail<J: Jail> {
jail: J,

// Lifecycle management fields (inlined from JailLifecycleManager)
// Lifecycle management fields
canary_dir: PathBuf,
canary_path: PathBuf,
heartbeat_interval: Duration,
Expand All @@ -26,9 +28,8 @@ pub struct ManagedJail<J: Jail> {
}

impl<J: Jail> ManagedJail<J> {
/// Create a new managed jail
pub fn new(jail: J, config: &JailConfig) -> Result<Self> {
let canary_dir = PathBuf::from("/tmp/httpjail");
let canary_dir = get_canary_dir();
let canary_path = canary_dir.join(&config.jail_id);

Ok(Self {
Expand Down
101 changes: 52 additions & 49 deletions src/jail/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use anyhow::Result;
use rand::Rng;

pub mod weak;

#[cfg(target_os = "linux")]
pub mod linux;

#[cfg(any(target_os = "macos", target_os = "linux"))]
pub mod managed;

/// Trait for platform-specific jail implementations
Expand Down Expand Up @@ -43,8 +49,18 @@ pub trait Jail: Send + Sync {
Self: Sized;
}

/// Configuration for jail setup
#[derive(Debug, Clone)]
/// Get the canary directory for tracking jail lifetimes
pub fn get_canary_dir() -> std::path::PathBuf {
std::path::PathBuf::from("/tmp/httpjail")
}

/// Get the directory for httpjail temporary files (like resolv.conf)
pub fn get_temp_dir() -> std::path::PathBuf {
std::path::PathBuf::from("/tmp/httpjail")
}

/// Jail configuration
#[derive(Clone, Debug)]
pub struct JailConfig {
/// Port where the HTTP proxy is listening
pub http_proxy_port: u16,
Expand Down Expand Up @@ -102,6 +118,40 @@ impl Default for JailConfig {
}
}

/// Create a platform-specific jail implementation wrapped with lifecycle management
pub fn create_jail(config: JailConfig, weak_mode: bool) -> Result<Box<dyn Jail>> {
use self::weak::WeakJail;

// Always use weak jail on macOS due to PF limitations
// (PF translation rules cannot match on user/group)
#[cfg(target_os = "macos")]
{
let _ = weak_mode; // Suppress unused warning on macOS
// WeakJail doesn't need lifecycle management since it creates no system resources
Ok(Box::new(WeakJail::new(config)?))
}

#[cfg(target_os = "linux")]
{
use self::linux::LinuxJail;
if weak_mode {
// WeakJail doesn't need lifecycle management since it creates no system resources
Ok(Box::new(WeakJail::new(config)?))
} else {
// LinuxJail creates system resources (namespaces, iptables) that need cleanup
Ok(Box::new(self::managed::ManagedJail::new(
LinuxJail::new(config.clone())?,
&config,
)?))
}
}

#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
anyhow::bail!("Unsupported platform")
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -144,50 +194,3 @@ mod tests {
assert_eq!(ids.len(), 1000);
}
}

// macOS module removed - using weak jail on macOS due to PF limitations
// (PF translation rules cannot match on user/group)

#[cfg(target_os = "linux")]
pub mod linux;

mod weak;

/// Create a platform-specific jail implementation wrapped with lifecycle management
pub fn create_jail(config: JailConfig, weak_mode: bool) -> Result<Box<dyn Jail>> {
use self::managed::ManagedJail;
use self::weak::WeakJail;

// Use weak jail if requested or on macOS (since PF cannot match groups with translation rules)
#[cfg(target_os = "macos")]
{
// Always use weak jail on macOS due to PF limitations
// (PF translation rules cannot match on user/group)
let _ = weak_mode; // Suppress unused warning on macOS
Ok(Box::new(ManagedJail::new(
WeakJail::new(config.clone())?,
&config,
)?))
}

#[cfg(target_os = "linux")]
{
if weak_mode {
Ok(Box::new(ManagedJail::new(
WeakJail::new(config.clone())?,
&config,
)?))
} else {
use self::linux::LinuxJail;
Ok(Box::new(ManagedJail::new(
LinuxJail::new(config.clone())?,
&config,
)?))
}
}

#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
anyhow::bail!("Unsupported platform")
}
}
18 changes: 8 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,12 @@ fn setup_logging(verbosity: u8) {
fn cleanup_orphans() -> Result<()> {
use anyhow::Context;
use std::fs;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use tracing::{debug, info};

let canary_dir = PathBuf::from("/tmp/httpjail");
let canary_dir = httpjail::jail::get_canary_dir();
let orphan_timeout = Duration::from_secs(5); // Short timeout to catch recent orphans

debug!("Starting direct orphan cleanup scan");
Expand Down Expand Up @@ -344,21 +345,17 @@ async fn main() -> Result<()> {
let mut parts = s.split_whitespace();
match (parts.next(), parts.next()) {
(Some(maybe_method), Some(url_rest)) => {
let method = maybe_method
.parse::<Method>()
.or_else(|_| maybe_method.to_ascii_uppercase().parse::<Method>())
.unwrap_or(Method::GET);
let method_str = maybe_method.to_ascii_uppercase();
let method = method_str.parse::<Method>().unwrap_or(Method::GET);
(method, url_rest.to_string())
}
_ => (Method::GET, s.clone()),
}
} else {
let maybe_method = &test_vals[0];
let url = &test_vals[1];
let method = maybe_method
.parse::<Method>()
.or_else(|_| maybe_method.to_ascii_uppercase().parse::<Method>())
.unwrap_or(Method::GET);
let method_str = maybe_method.to_ascii_uppercase();
let method = method_str.parse::<Method>().unwrap_or(Method::GET);
(method, url.clone())
};

Expand Down Expand Up @@ -448,7 +445,8 @@ async fn main() -> Result<()> {
}

// Create jail canary dir early to reduce race with cleanup
std::fs::create_dir_all("/tmp/httpjail").ok();
let canary_dir = httpjail::jail::get_canary_dir();
std::fs::create_dir_all(&canary_dir).ok();

// Configure and execute the target command inside a jail
jail_config.http_proxy_port = actual_http_port;
Expand Down
Loading
Loading