diff --git a/CLAUDE.md b/CLAUDE.md index c6f85481..247d4dd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,6 +93,10 @@ The CI workspace is located at `/home/ci/actions-runner/_work/httpjail/httpjail` # SCP files to/from CI-1 ./scripts/ci-scp.sh src/ /tmp/httpjail-docker-run/ # Upload ./scripts/ci-scp.sh root@ci-1:/path/to/file ./ # Download + +# Wait for PR checks to pass or fail +./scripts/wait-pr-checks.sh 47 # Monitor PR #47 +./scripts/wait-pr-checks.sh 47 coder/httpjail # Specify repo explicitly ``` ### Manual Testing on CI diff --git a/scripts/wait-pr-checks.sh b/scripts/wait-pr-checks.sh new file mode 100755 index 00000000..166003bd --- /dev/null +++ b/scripts/wait-pr-checks.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# wait-pr-checks.sh - Poll GitHub Actions status for a PR and exit on first failure or when all pass +# +# Usage: ./scripts/wait-pr-checks.sh [repo] +# pr-number: The PR number to check +# repo: Optional repository in format owner/repo (defaults to coder/httpjail) +# +# Exit codes: +# 0 - All checks passed +# 1 - A check failed +# 2 - Invalid arguments +# +# Requires: gh, jq + +set -euo pipefail + +# Parse arguments +if [ $# -lt 1 ]; then + echo "Usage: $0 [repo]" >&2 + echo "Example: $0 47" >&2 + echo "Example: $0 47 coder/httpjail" >&2 + exit 2 +fi + +PR_NUMBER="$1" +REPO="${2:-coder/httpjail}" + +# Check for required tools +if ! command -v jq &> /dev/null; then + echo "Error: jq is required but not installed" >&2 + exit 2 +fi + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "Monitoring PR #${PR_NUMBER} in ${REPO}..." +echo "Polling every second. Press Ctrl+C to stop." +echo "" + +# Track the last status to avoid duplicate output +last_status="" + +while true; do + # Get check status as JSON + if ! json_output=$(gh pr checks "${PR_NUMBER}" --repo "${REPO}" --json name,state,link 2>/dev/null); then + echo -e "${YELLOW}Waiting for checks to start...${NC}" + sleep 1 + continue + fi + + # Parse JSON to get counts + pending_count=$(echo "$json_output" | jq '[.[] | select(.state == "PENDING" or .state == "IN_PROGRESS" or .state == "QUEUED")] | length') + failed_count=$(echo "$json_output" | jq '[.[] | select(.state == "FAILURE" or .state == "ERROR")] | length') + passed_count=$(echo "$json_output" | jq '[.[] | select(.state == "SUCCESS")] | length') + total_count=$(echo "$json_output" | jq 'length') + + # Build status string + current_status="✓ ${passed_count} passed | ⏳ ${pending_count} pending | ✗ ${failed_count} failed" + + # Only print if status changed + if [ "$current_status" != "$last_status" ]; then + echo -ne "\r\033[K${current_status}" + last_status="$current_status" + fi + + # Check for failures + if [ $failed_count -gt 0 ]; then + echo -e "\n\n${RED}❌ The following check(s) failed:${NC}" + echo "$json_output" | jq -r '.[] | select(.state == "FAILURE" or .state == "ERROR") | " - \(.name)"' + echo -e "\nView details at: https://github.com/${REPO}/pull/${PR_NUMBER}/checks" + exit 1 + fi + + # Check if all passed + if [ $total_count -gt 0 ] && [ $pending_count -eq 0 ] && [ $failed_count -eq 0 ]; then + echo -e "\n\n${GREEN}✅ All ${passed_count} checks passed!${NC}" + echo -e "PR #${PR_NUMBER} is ready to merge." + exit 0 + fi + + sleep 1 +done \ No newline at end of file diff --git a/src/jail/linux/mod.rs b/src/jail/linux/mod.rs index 582105a1..25d9ae90 100644 --- a/src/jail/linux/mod.rs +++ b/src/jail/linux/mod.rs @@ -623,72 +623,45 @@ impl Jail for LinuxJail { // Check if we're running as root and should drop privileges let current_uid = unsafe { libc::getuid() }; - let target_user = if current_uid == 0 { - // Running as root - check for SUDO_USER to drop privileges to original user - std::env::var("SUDO_USER").ok() + let drop_privs = if current_uid == 0 { + // Running as root - check for SUDO_UID/SUDO_GID to drop privileges to original user + match (std::env::var("SUDO_UID"), std::env::var("SUDO_GID")) { + (Ok(uid), Ok(gid)) => { + debug!( + "Will drop privileges to uid={} gid={} after entering namespace", + uid, gid + ); + Some((uid, gid)) + } + _ => { + debug!("Running as root but no SUDO_UID/SUDO_GID found, continuing as root"); + None + } + } } else { // Not root - no privilege dropping needed None }; - if let Some(ref user) = target_user { - debug!( - "Will drop to user '{}' (from SUDO_USER) after entering namespace", - user - ); - } - // Build command: ip netns exec - // If we need to drop privileges, we wrap with su + // If we need to drop privileges, we wrap with setpriv let mut cmd = Command::new("ip"); cmd.args(["netns", "exec", &self.namespace_name()]); - // When we have environment variables to pass OR need to drop privileges, - // use a shell wrapper to ensure proper environment handling - if target_user.is_some() || !extra_env.is_empty() { - // Build shell command with explicit environment exports - let mut shell_command = String::new(); - - // Export environment variables explicitly in the shell command - for (key, value) in extra_env { - // Escape the value for shell safety - let escaped_value = value.replace('\'', "'\\''"); - shell_command.push_str(&format!("export {}='{}'; ", key, escaped_value)); - } - - // Add the actual command with proper escaping - shell_command.push_str( - &command - .iter() - .map(|arg| { - // Simple escaping: wrap in single quotes and escape existing single quotes - if arg.contains('\'') { - format!("\"{}\"", arg.replace('"', "\\\"")) - } else { - format!("'{}'", arg) - } - }) - .collect::>() - .join(" "), - ); - - if let Some(user) = target_user { - // Use su to drop privileges to the original user - cmd.arg("su"); - cmd.arg("-s"); // Specify shell explicitly - cmd.arg("/bin/sh"); // Use sh for compatibility - cmd.arg("-p"); // Preserve environment - cmd.arg(&user); // Username from SUDO_USER - cmd.arg("-c"); // Execute command - cmd.arg(shell_command); - } else { - // No privilege dropping but need shell for env vars - cmd.arg("sh"); - cmd.arg("-c"); - cmd.arg(shell_command); + // Handle privilege dropping and command execution + if let Some((uid, gid)) = drop_privs { + // Use setpriv to drop privileges to the original user + // setpriv is lighter than runuser - no PAM, direct execve() + cmd.arg("setpriv"); + cmd.arg(format!("--reuid={}", uid)); // Set real and effective UID + cmd.arg(format!("--regid={}", gid)); // Set real and effective GID + cmd.arg("--init-groups"); // Initialize supplementary groups + cmd.arg("--"); // End of options + for arg in command { + cmd.arg(arg); } } else { - // No privilege dropping and no env vars, execute directly + // No privilege dropping, execute directly cmd.arg(&command[0]); for arg in &command[1..] { cmd.arg(arg); @@ -711,6 +684,8 @@ impl Jail for LinuxJail { cmd.env("SUDO_GID", sudo_gid); } + debug!("Executing command: {:?}", cmd); + // Note: We do NOT set HTTP_PROXY/HTTPS_PROXY environment variables here. // The jail uses nftables rules to transparently redirect traffic to the proxy, // making it work with applications that don't respect proxy environment variables.