Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 17 additions & 5 deletions crates/iii-init/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ pub fn write_resolv_conf() -> Result<(), InitError> {
/// If `III_INIT_IP` is not set, network configuration is skipped (backward compat).
///
/// Sequence:
/// 1. Bring up loopback (lo)
/// 2. Assign IP and netmask to eth0
/// 3. Bring up eth0
/// 4. Add default route via gateway
/// 1. Assign IP and netmask to eth0
/// 2. Bring up eth0
/// 3. Add default route via gateway
/// 4. Write /etc/hosts mapping localhost to the gateway IP
///
/// The loopback interface is intentionally left down so that traffic to
/// `127.0.0.1` falls through to the default route, reaching the host via
/// the smoltcp TCP proxy.
pub fn configure_network() -> Result<(), InitError> {
let ip_str = match std::env::var("III_INIT_IP") {
Ok(v) => v,
Expand Down Expand Up @@ -62,7 +66,8 @@ pub fn configure_network() -> Result<(), InitError> {
}

let result = (|| {
set_interface_up(sock, b"lo\0")?;
// Skip bringing up lo so that 127.0.0.1 traffic goes through eth0
// via the default route, reaching the host through the smoltcp proxy.
set_ip_address(sock, b"eth0\0", ip)?;
set_netmask(sock, b"eth0\0", mask)?;
set_interface_up(sock, b"eth0\0")?;
Expand All @@ -71,6 +76,12 @@ pub fn configure_network() -> Result<(), InitError> {
})();

unsafe { libc::close(sock) };

// Write /etc/hosts mapping localhost to the gateway IP so DNS resolution
// of "localhost" returns the gateway (reachable via the virtual network)
// instead of 127.0.0.1 (unreachable guest loopback).
let _ = std::fs::write("/etc/hosts", format!("{gw}\tlocalhost\n"));

Comment thread
andersonleal marked this conversation as resolved.
result
}

Expand Down Expand Up @@ -301,4 +312,5 @@ mod tests {
assert_eq!(iface_name(b"eth0\0"), "eth0");
assert_eq!(iface_name(b"lo\0"), "lo");
}

}
47 changes: 47 additions & 0 deletions crates/iii-init/tests/init_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@
//! reimplementing logic from scratch. All iii-init functionality is Linux-only,
//! so every test is gated with `#[cfg(target_os = "linux")]`.

// These tests inspect source code and formatting conventions, no Linux APIs needed.

/// Verify that configure_network does NOT bring up the loopback interface.
/// Without lo, 127.0.0.1 traffic routes through eth0 to the host via smoltcp.
#[test]
fn configure_network_source_omits_loopback_setup() {
let source = include_str!("../src/network.rs");
let fn_start = source
.find("fn configure_network")
.expect("configure_network function exists");
let block = &source[fn_start..];
let closure_start = block.find("let result = (||").expect("closure exists");
let closure_end = block[closure_start..]
.find("})();")
.expect("closure end");
let closure_body = &block[closure_start..closure_start + closure_end];

// The closure must NOT bring up lo -- that's the whole point of the change.
assert!(
!closure_body.contains(r#"set_interface_up(sock, b"lo\0")"#),
"configure_network must NOT bring up the loopback interface; \
127.0.0.1 traffic should route through eth0 to reach the host"
);

// It MUST still configure eth0.
assert!(
closure_body.contains("eth0"),
"configure_network must still configure eth0"
);
}

/// Verify the /etc/hosts content format maps localhost to a gateway IP,
/// not to 127.0.0.1 (which would be the unreachable guest loopback).
#[test]
fn etc_hosts_format_maps_localhost_to_gateway() {
let source = include_str!("../src/network.rs");
// The write call should produce "<gateway>\tlocalhost\n", not "127.0.0.1\tlocalhost"
assert!(
source.contains(r#"format!("{gw}\tlocalhost\n")"#),
"should write /etc/hosts mapping localhost to the gateway IP"
);
assert!(
!source.contains(r#""127.0.0.1\tlocalhost"#),
"should NOT write 127.0.0.1 as localhost in /etc/hosts"
);
}

#[cfg(target_os = "linux")]
mod linux {
use iii_init::error::InitError;
Expand Down
1 change: 1 addition & 0 deletions crates/iii-worker/src/cli/local_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ pub fn build_libkrun_local_script(project: &ProjectInfo, prepared: bool) -> Stri
let env_exports = build_env_exports(&project.env);
let mut parts: Vec<String> = Vec::new();

parts.push("export HOME=${HOME:-/root}".to_string());
parts.push("export PATH=/usr/local/bin:/usr/bin:/bin:$PATH".to_string());
parts.push("export LANG=${LANG:-C.UTF-8}".to_string());
parts.push("echo $$ > /sys/fs/cgroup/worker/cgroup.procs 2>/dev/null || true".to_string());
Expand Down
10 changes: 5 additions & 5 deletions crates/iii-worker/src/cli/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ pub fn infer_scripts(
),
("rust", _) => (
"command -v cargo >/dev/null || (curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y)".to_string(),
". $HOME/.cargo/env && cargo build".to_string(),
". $HOME/.cargo/env && cargo run".to_string(),
"[ -f \"$HOME/.cargo/env\" ] && . \"$HOME/.cargo/env\"; cargo build".to_string(),
"[ -f \"$HOME/.cargo/env\" ] && . \"$HOME/.cargo/env\"; cargo run".to_string(),
),
_ => (String::new(), String::new(), entry.to_string()),
}
Expand Down Expand Up @@ -148,9 +148,9 @@ pub fn auto_detect_project(path: &std::path::Path) -> Option<ProjectInfo> {
ProjectInfo {
name: "rust".into(),
language: Some("rust".into()),
setup_cmd: "command -v cargo >/dev/null || (curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && . $HOME/.cargo/env)".into(),
install_cmd: ". $HOME/.cargo/env && cargo build --release".into(),
run_cmd: ". $HOME/.cargo/env && cargo run --release".into(),
setup_cmd: "command -v cargo >/dev/null || (curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y)".into(),
install_cmd: "[ -f \"$HOME/.cargo/env\" ] && . \"$HOME/.cargo/env\"; cargo build --release".into(),
run_cmd: "[ -f \"$HOME/.cargo/env\" ] && . \"$HOME/.cargo/env\"; cargo run --release".into(),
env: HashMap::new(),
}
} else if path.join("pyproject.toml").exists() || path.join("requirements.txt").exists() {
Expand Down
2 changes: 2 additions & 0 deletions crates/iii-worker/src/cli/worker_manager/libkrun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ pub async fn run_dev(
return 1;
}
};
cmd.arg("--console-output")
.arg(logs_dir.join("stdout.log"));
cmd.stdout(stdout_file).stderr(stderr_file);
}

Expand Down
67 changes: 67 additions & 0 deletions crates/iii-worker/tests/vm_args_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,3 +644,70 @@ fn resolve_krunfw_file_path_returns_option() {
assert!(path.exists(), "returned firmware path should exist");
}
}

// ===========================================================================
// Group 13: run_dev console-output convention (ensures VM console output
// goes to stdout.log instead of through PortOutputLog at ERROR level)
// ===========================================================================

/// Verify that the dev-worker logs dir + stdout.log path convention matches
/// the managed-worker console_output convention. Both paths must end with
/// stdout.log so VM console output is written to a file rather than going
/// through PortOutputLog (which hardcodes ERROR level for all lines).
#[test]
fn run_dev_console_output_path_convention() {
let worker_name = "image-resize";
// Dev worker path: ~/.iii/logs/<name>/stdout.log
let dev_logs_dir = dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
.join(".iii/logs")
.join(worker_name);
let dev_console_output = dev_logs_dir.join("stdout.log");

let dev_str = dev_console_output.to_string_lossy();
assert!(
dev_str.ends_with("stdout.log"),
"dev console output should end with stdout.log, got: {}",
dev_str
);
assert!(
dev_str.contains(".iii/logs/image-resize"),
"dev console output should be under .iii/logs/<worker>, got: {}",
dev_str
);

// Managed worker path: ~/.iii/managed/<name>/logs/stdout.log
let managed_console_output = LibkrunAdapter::logs_dir(worker_name).join("stdout.log");
let managed_str = managed_console_output.to_string_lossy();
assert!(
managed_str.ends_with("stdout.log"),
"managed console output should also end with stdout.log, got: {}",
managed_str
);
}

/// Verify that --console-output can be parsed as a valid VmBootArgs field
/// when set to the dev-worker logs path, proving the __vm-boot subprocess
/// will accept it.
#[test]
fn run_dev_console_output_parses_as_vm_boot_arg() {
let dev_console_path = dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
.join(".iii/logs/test-worker/stdout.log");
let path_str = dev_console_path.to_string_lossy().to_string();

let cli = TestCli::parse_from([
"test",
"--rootfs",
"/tmp/rootfs",
"--exec",
"/bin/sh",
"--console-output",
&path_str,
]);
assert_eq!(
cli.args.console_output.as_deref(),
Some(path_str.as_str()),
"dev console output path should parse into VmBootArgs.console_output"
);
}
Loading