Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions scripts/wait-pr-checks.sh
Original file line number Diff line number Diff line change
@@ -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 <pr-number> [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 <pr-number> [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
85 changes: 30 additions & 55 deletions src/jail/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <namespace> <command>
// 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::<Vec<_>>()
.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);
Expand All @@ -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.
Expand Down
Loading