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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ Thumbs.db

# --- Logs ---
*.log

# --- Cached Images ---
cached_images/
37 changes: 37 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Justfile
# https://github.com/casey/just

default:
@just --list

build:
cargo build --release

fmt:
cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
# cargo shear --fix # first install shear: cargo install shear

check:
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings

install-hook:
#!/usr/bin/env bash
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
set -e
echo "Running pre-commit quality checks..."
just check
EOF
chmod +x .git/hooks/pre-commit
echo "Pre-commit hook installation confirmed."

remove-hook:
rm .git/hooks/pre-commit
echo "Pre-commit hook uninstallation confirmed."

# Run unit tests
test: fmt
cargo test

42 changes: 25 additions & 17 deletions pull_image.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import subprocess
import os
import tarfile
import urllib.request
import shutil
import sys
Expand All @@ -11,39 +11,47 @@
"debian": "https://github.com/debuerreotype/docker-debian-artifacts/raw/dist-amd64/bookworm/rootfs.tar.xz"
}

CACHE_DIR = "./cached_images"

def setup_rootfs(distro_name, target_dir="./rootfs"):
if distro_name not in DISTROS:
print(f"Error: Distro '{distro_name}' not supported. Choose from: {list(DISTROS.keys())}")
return

# Create cache dir if it doesn't exist
if not os.path.exists(CACHE_DIR):
os.makedirs(CACHE_DIR)

# 1. Cleanup old rootfs
if os.path.exists(target_dir):
print(f"Cleaning up old {target_dir}...")
shutil.rmtree(target_dir)
os.makedirs(target_dir)

# 2. Download
# 2. Check Cache or Download
url = DISTROS[distro_name]
file_name = f"base_image.tar.gz"
print(f"Downloading {distro_name} from {url}...")

try:
urllib.request.urlretrieve(url, file_name)
print("Download complete.")
# Simple extension detection
ext = ".tar.gz" if ".tar.gz" in url else ".tar.xz"
cache_path = os.path.join(CACHE_DIR, f"{distro_name}{ext}")

# 3. Extract
if os.path.exists(cache_path):
print(f"Using cached image: {cache_path}")
else:
print(f"Downloading {distro_name} from {url}...")
try:
urllib.request.urlretrieve(url, cache_path)
print("Download complete.")
except Exception as e:
print(f"Error downloading image: {e}")
return

# 3. Extract
try:
print(f"Extracting to {target_dir}...")
with tarfile.open(file_name) as tar:
tar.extractall(path=target_dir)

subprocess.run(["tar", "-xf", cache_path, "-C", target_dir], check=True)
print(f"Success! {distro_name} is ready in {target_dir}")

except Exception as e:
print(f"Error: {e}")
finally:
# 4. Cleanup the tar file
if os.path.exists(file_name):
os.remove(file_name)

if __name__ == "__main__":
name = sys.argv[1] if len(sys.argv) > 1 else "alpine"
Expand Down
6 changes: 5 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use clap::Parser;

#[derive(Parser, Debug, Clone)]
#[command(author, version, about = "Nucleus: High-performance Rust Container Engine")]
#[command(
author,
version,
about = "Nucleus: High-performance Rust Container Engine"
)]
pub struct OxideArgs {
/// Unique name for the container instance
#[arg(short, long)]
Expand Down
139 changes: 108 additions & 31 deletions src/container.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
use crate::args::OxideArgs;
use anyhow::{Context, Result};
use caps::{CapSet, Capability};
use nix::mount::{mount, MsFlags, umount2, MntFlags};
use nix::sched::{unshare, CloneFlags};
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::{chdir, execvp, read, sethostname, pivot_root, fork, ForkResult};
use nix::mount::{MntFlags, MsFlags, mount, umount2};
use nix::sched::{CloneFlags, unshare};
use nix::sys::wait::{WaitStatus, waitpid};
use nix::unistd::{ForkResult, chdir, execvp, fork, pivot_root, read, sethostname};
use std::ffi::CString;
use std::fs;
use std::os::unix::io::RawFd;
use std::path::Path;
use crate::args::OxideArgs;

/// Child Context: Isolates itself and prepares the container environment.
pub fn run_container_child(args: OxideArgs) -> Result<()> {
// 1. Isolate BEFORE doing anything else
unshare(CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUTS | CloneFlags::CLONE_NEWPID | CloneFlags::CLONE_NEWNET | CloneFlags::CLONE_NEWCGROUP)
.context("Failed to isolate child namespaces")?;
unshare(
CloneFlags::CLONE_NEWNS
| CloneFlags::CLONE_NEWUTS
| CloneFlags::CLONE_NEWPID
| CloneFlags::CLONE_NEWNET
| CloneFlags::CLONE_NEWCGROUP,
)
.context("Failed to isolate child namespaces")?;

// 2. Fork into the new PID namespace
// In Linux, the process that calls unshare(CLONE_NEWPID) doesn't enter the namespace,
// In Linux, the process that calls unshare(CLONE_NEWPID) doesn't enter the namespace,
// but its next child becomes PID 1.
match unsafe { fork() }.context("Failed to fork after unshare")? {
ForkResult::Parent { child } => {
Expand All @@ -38,8 +44,14 @@ pub fn run_container_child(args: OxideArgs) -> Result<()> {

fn setup_container_env(args: OxideArgs) -> Result<()> {
// Fix pivot_root EINVAL: Ensure our mount namespace is private
mount(None::<&str>, "/", None::<&str>, MsFlags::MS_REC | MsFlags::MS_PRIVATE, None::<&str>)
.context("Failed to set mount propagation to private")?;
mount(
None::<&str>,
"/",
None::<&str>,
MsFlags::MS_REC | MsFlags::MS_PRIVATE,
None::<&str>,
)
.context("Failed to set mount propagation to private")?;

// Sync with Parent: Wait for host-side networking to be ready
let pipe_fd = args.pipe_fd.context("Missing pipe handle")?;
Expand All @@ -51,22 +63,35 @@ fn setup_container_env(args: OxideArgs) -> Result<()> {

// Layered Filesystem (OverlayFS)
let root_base = format!("./temp/{}", args.name);
let _ = fs::remove_dir_all(&root_base);
let upper = format!("{}/upper", root_base);
let work = format!("{}/work", root_base);
let merged = format!("{}/merged", root_base);

fs::create_dir_all(&upper).ok();
fs::create_dir_all(&work).ok();
fs::create_dir_all(&merged).ok();

let overlay_opts = format!("lowerdir=./rootfs,upperdir={},workdir={}", upper, work);
mount(Some("overlay"), merged.as_str(), Some("overlay"), MsFlags::empty(), Some(overlay_opts.as_str()))
.context("Failed to mount OverlayFS")?;
mount(
Some("overlay"),
merged.as_str(),
Some("overlay"),
MsFlags::empty(),
Some(overlay_opts.as_str()),
)
.context("Failed to mount OverlayFS")?;

// Pivot Root
mount(Some(merged.as_str()), merged.as_str(), None::<&str>, MsFlags::MS_BIND | MsFlags::MS_REC, None::<&str>)
.context("Failed to bind mount root for pivot_root")?;

mount(
Some(merged.as_str()),
merged.as_str(),
None::<&str>,
MsFlags::MS_BIND | MsFlags::MS_REC,
None::<&str>,
)
.context("Failed to bind mount root for pivot_root")?;

let old_root_name = ".old_root";
let old_root_path = Path::new(&merged).join(old_root_name);
fs::create_dir_all(&old_root_path).context("Failed to create old_root dir")?;
Expand All @@ -75,23 +100,53 @@ fn setup_container_env(args: OxideArgs) -> Result<()> {
chdir("/").context("Failed to chdir to new root")?;

let old_root_path_in_container = format!("/{}", old_root_name);
umount2(old_root_path_in_container.as_str(), MntFlags::MNT_DETACH).context("Failed to unmount old root")?;
umount2(old_root_path_in_container.as_str(), MntFlags::MNT_DETACH)
.context("Failed to unmount old root")?;
fs::remove_dir(old_root_path_in_container.as_str()).ok();

// System Mounts (Procfs, Sysfs, DNS, Volumes)
mount(Some("proc"), "/proc", Some("proc"), MsFlags::empty(), None::<&str>).context("Failed to mount proc")?;

fs::create_dir_all("/proc").ok();
mount(
Some("proc"),
"/proc",
Some("proc"),
MsFlags::empty(),
None::<&str>,
)
.context("Failed to mount proc")?;
fs::create_dir_all("/etc").ok();

fs::create_dir_all("/sys").ok();
mount(Some("sysfs"), "/sys", Some("sysfs"), MsFlags::empty(), None::<&str>).context("Failed to mount sysfs")?;
mount(
Some("sysfs"),
"/sys",
Some("sysfs"),
MsFlags::empty(),
None::<&str>,
)
.context("Failed to mount sysfs")?;

fs::create_dir_all("/sys/fs/cgroup").ok();
mount(Some("cgroup2"), "/sys/fs/cgroup", Some("cgroup2"), MsFlags::empty(), None::<&str>).context("Failed to mount cgroup2")?;

mount(
Some("cgroup2"),
"/sys/fs/cgroup",
Some("cgroup2"),
MsFlags::empty(),
None::<&str>,
)
.context("Failed to mount cgroup2")?;

let resolv_conf = "/etc/resolv.conf";
if Path::new(resolv_conf).exists() {
fs::File::create(resolv_conf).ok();
mount(Some(resolv_conf), resolv_conf, None::<&str>, MsFlags::MS_BIND | MsFlags::MS_RDONLY, None::<&str>)
.context("Failed to bind mount resolv.conf")?;
fs::File::create(resolv_conf).ok();
mount(
Some(resolv_conf),
resolv_conf,
None::<&str>,
MsFlags::MS_BIND | MsFlags::MS_RDONLY,
None::<&str>,
)
.context("Failed to bind mount resolv.conf")?;
}

// Bind User Volumes: -v /host:/container
Expand All @@ -104,23 +159,45 @@ fn setup_container_env(args: OxideArgs) -> Result<()> {
} else {
format!("/{}", parts[1])
};

fs::create_dir_all(&container_path).ok();
mount(Some(host_path), container_path.as_str(), None::<&str>, MsFlags::MS_BIND | MsFlags::MS_REC, None::<&str>)
.context(format!("Failed to bind mount volume: {}", vol))?;
mount(
Some(host_path),
container_path.as_str(),
None::<&str>,
MsFlags::MS_BIND | MsFlags::MS_REC,
None::<&str>,
)
.context(format!("Failed to bind mount volume: {}", vol))?;
}
}

// Security: Drop dangerous capabilities
drop_capabilities()?;

// Setup Environment Variables
unsafe {
std::env::set_var(
"PATH",
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
);
std::env::set_var("HOME", "/root");
std::env::set_var("USER", "root");
std::env::remove_var("PS1");
std::env::remove_var("PROMPT");
}

// Execute Target Command
println!("[Container] Entering {}...", args.command[0]);
let cmd = CString::new(args.command[0].as_str()).unwrap();
let c_args: Vec<CString> = args.command.iter().map(|s| CString::new(s.as_str()).unwrap()).collect();

let c_args: Vec<CString> = args
.command
.iter()
.map(|s| CString::new(s.as_str()).unwrap())
.collect();

execvp(&cmd, &c_args).context("Failed to execute inner command")?;

Ok(())
}

Expand Down
10 changes: 6 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
mod args;
mod orchestrator;
mod container;
mod orchestrator;
mod utils;

use crate::args::OxideArgs;
use anyhow::Result;
use clap::Parser;
use nix::unistd::getuid;
use crate::args::OxideArgs;

fn main() -> Result<()> {
// 1. Startup Verification: Check for root
// Nucleus needs root for namespaces, mounts, and networking
if !getuid().is_root() {
return Err(anyhow::anyhow!("Nucleus must be run as root to manage namespaces and networking."));
return Err(anyhow::anyhow!(
"Nucleus must be run as root to manage namespaces and networking."
));
}

// 2. Parse CLI Arguments
Expand All @@ -26,6 +28,6 @@ fn main() -> Result<()> {
// Otherwise, we are the host-side orchestrator
orchestrator::run_parent_orchestrator(args)?;
}

Ok(())
}
Loading
Loading