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
28 changes: 14 additions & 14 deletions README.md

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions crates/chefer-runtime/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,18 @@ fn unpack_tar<R: Read>(reader: R, dest: &Path) -> Result<()> {
.with_context(|| format!("failed to create file: {}", out.display()))?;
io::copy(&mut entry, &mut file)
.with_context(|| format!("failed to write file: {}", out.display()))?;
// 還原 tar header 的 mode:bundle/agents/ 下的 helper/guest-agent/pasta
// 是 0o755,macOS host 要直接 spawn(vz-helper)、guest 也經 virtiofs 執行,
// 不還原就 EACCES。Windows 無 POSIX mode,略過。
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = entry.header().mode().unwrap_or(0o644) & 0o777;
file.set_permissions(std::fs::Permissions::from_mode(mode))
.with_context(|| {
format!("failed to set permissions on: {}", out.display())
})?;
}
}
other => {
bail!(
Expand Down Expand Up @@ -421,6 +433,64 @@ mod tests {
assert!(!bundle_dir.exists(), "未指定 --keep-tmp 時應自動刪除暫存");
}

/// Unix:解壓須還原 tar header 的 mode——bundle/agents/ 的 0o755 掉了會讓
/// macOS host spawn vz-helper/guest 執行 guest-agent 直接 EACCES。
#[cfg(unix)]
#[test]
fn extract_restores_exec_mode() {
use std::os::unix::fs::PermissionsExt;

let mut tar_buf = Vec::new();
{
let mut b = tar::Builder::new(&mut tar_buf);
for dir in ["bundle/", "bundle/agents/"] {
let mut h = tar::Header::new_gnu();
h.set_entry_type(tar::EntryType::Directory);
h.set_mode(0o755);
h.set_size(0);
h.set_mtime(0);
b.append_data(&mut h, dir, io::empty()).unwrap();
}
let mut h = tar::Header::new_gnu();
h.set_entry_type(tar::EntryType::Regular);
h.set_mode(0o644);
h.set_size(2);
h.set_mtime(0);
b.append_data(&mut h, "bundle/manifest.json", &b"{}"[..])
.unwrap();
let mut h = tar::Header::new_gnu();
h.set_entry_type(tar::EntryType::Regular);
h.set_mode(0o755);
h.set_size(5);
h.set_mtime(0);
b.append_data(&mut h, "bundle/agents/fake-agent", &b"#!bin"[..])
.unwrap();
b.finish().unwrap();
}
let payload = zstd::stream::encode_all(&tar_buf[..], 3).unwrap();

let tmp = tempfile::tempdir().unwrap();
let exe = tmp.path().join("fake-app.exe");
let ft = write_fake_single(&exe, 16, &payload);
let parent = tmp.path().join("work");
let opts = ExtractOptions {
extract_parent: Some(&parent),
keep_tmp: false,
};
let extracted = extract_bundle(&exe, &ft, &opts).unwrap();

let agent_mode = fs::metadata(extracted.bundle_dir.join("agents/fake-agent"))
.unwrap()
.permissions()
.mode();
assert_eq!(agent_mode & 0o777, 0o755, "agents/ 執行檔應還原 0o755");
let manifest_mode = fs::metadata(extracted.bundle_dir.join("manifest.json"))
.unwrap()
.permissions()
.mode();
assert_eq!(manifest_mode & 0o777, 0o644, "資料檔應為 0o644");
}

#[test]
fn keep_tmp_preserves_dir() {
let tmp = tempfile::tempdir().unwrap();
Expand Down
8 changes: 8 additions & 0 deletions crates/chefer-runtime/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ use chefer_bundle::{Manifest, PortProto};
/// 任一 host 埠 bind 失敗(通常是埠已被占用)→ 啟動前整體報錯並列出埠號,
/// 不會啟動任何服務。
pub fn start_port_proxies(manifest: &Manifest) -> Result<()> {
// macOS(vz):guest 埠不在 host localhost 上(VZ NAT 無 wslrelay 式自動轉送),
// host→guest 一律由 vz 後端取得 guest IP 後 relay `127.0.0.1:host → <vm_ip>:guest`
// (TCP+UDP、含 host==guest)。在此綁 127.0.0.1:host 只會與後端的 relay 搶同一顆埠
// (同 Windows UDP 的先例),故整個略過。見 DESIGN「埠代理」。
if cfg!(target_os = "macos") {
tracing::debug!("port proxies are handled by the vz backend on macOS; skipping");
return Ok(());
}
let mut failures: Vec<String> = Vec::new();
for svc in &manifest.services {
for spec in &svc.ports {
Expand Down
31 changes: 31 additions & 0 deletions crates/guest-agent/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,37 @@ fn build_plan(spec: &SpawnSpec) -> Result<ChildPlan> {
});
}

// DNS:容器 rootfs 沒有 Docker daemon 幫忙注入 /etc/resolv.conf(alpine 等基底 image
// 也不自帶),服務內任何 hostname 解析都會倒在預設的 127.0.0.1:53 → 直接失敗。
// 比照 Docker:把執行環境(VM root / 原生 host)的 /etc/resolv.conf 注入容器。
// 兩個坑決定了做法「fork 前實體化內容到 temp 檔,再 bind 那個檔」:
// - 不能 fork 前直接寫進 spec.rootfs——rootfs 的 overlay 是孫行程在自己的 mount ns
// 內才蓋上去的,先寫會被 overlay 遮住;bind 在 overlay 之後套用才可見。
// - 不能直接 bind /etc/resolv.conf——VM 後端(vz/QEMU/WHP)的 appliance init 把它
// symlink 到 /proc/net/pnp,而孫行程 bind 時已 setns 進 app netns,/proc/net 是
// 該 netns 的視圖、pnp 不存在 → ENOENT。故在此(root netns)先讀出內容寫成一般檔案。
// image 自帶的 resolv.conf 一律被蓋掉(通常是 build 環境殘留,如 Docker 內嵌的
// 127.0.0.11);執行環境沒有該檔或內容為空時不注入、維持原狀。
if let Ok(dns) = fs::read_to_string("/etc/resolv.conf")
&& !dns.trim().is_empty()
{
let staged =
std::env::temp_dir().join(format!("chefer-resolv-{}-{}", std::process::id(), svc.name));
match fs::write(&staged, &dns) {
Ok(()) => binds.push(BindEntry {
host: staged,
target: join_guest(spec.rootfs, "/etc/resolv.conf")?,
read_only: false,
is_dir: false,
}),
Err(e) => eprintln!(
"[guest-agent] service `{}`: could not stage /etc/resolv.conf for the container ({e}); \
name resolution inside the service may fail",
svc.name
),
}
}

// GPU passthrough(opt-in per-service `gpu: true`;見 docs/DESIGN.md「GPU passthrough」)。
// 只在能實際觸及 host GPU 的後端可行(原生 Linux / WSL2);WHP/vz VM 明確報錯,
// 不靜默綁到 virtio-gpu 的 2D `/dev/dri`。探測到的節點/libs 以既有 bind 流程套用
Expand Down
13 changes: 10 additions & 3 deletions crates/vmm-backend/src/whp_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ pub fn helper_in_bundle(bundle_dir: &Path, host_arch: &str) -> PathBuf {
/// 網路:用 kernel IP_PNP **靜態**設定 eth0(`10.0.2.15/24`、gateway `10.0.2.2`),
/// 不走 DHCP——helper 的 smoltcp gateway 不提供 DHCP server。此 IP 與 gateway 必須與
/// `whp-helper` 的 `NET_GUEST_IP`/`NET_GATEWAY_IP` 一致(host↔guest 網路契約)。
///
/// `ip=` 第 8 欄(dns0)帶約定 DNS `10.0.2.3`(slirp 慣例位址;helper 在
/// `net_backend::NAT_DNS_IP` 提供轉發):kernel ipconfig 對靜態設定**也會**建
/// `/proc/net/pnp`(無 dns0 時內容只有 `#MANUAL`、無 nameserver 行),dns0 讓它多出
/// `nameserver 10.0.2.3` → appliance init 的 resolv.conf symlink + guest-agent 的
/// 容器注入整條沿用 vz/QEMU 的既有鏈(見 docs/DESIGN.md「容器內 DNS」)。
pub fn kernel_command_line(keep_rootfs: bool) -> String {
let mut s = String::from(
"console=ttyS0 quiet ip=10.0.2.15::10.0.2.2:255.255.255.0::eth0:off \
"console=ttyS0 quiet ip=10.0.2.15::10.0.2.2:255.255.255.0::eth0:off:10.0.2.3 \
panic=-1 nolapic lpj=1000000 notsc clocksource=jiffies",
);
s.push_str(&format!(
Expand Down Expand Up @@ -237,8 +243,9 @@ mod tests {
let cmdline = kernel_command_line(true);

assert!(cmdline.contains("console=ttyS0"));
// WHP 用 kernel 靜態 IP(無 DHCP server),IP/gw 對齊 helper 的 net 契約。
assert!(cmdline.contains("ip=10.0.2.15::10.0.2.2:255.255.255.0::eth0:off"));
// WHP 用 kernel 靜態 IP(無 DHCP server),IP/gw 對齊 helper 的 net 契約;
// dns0=10.0.2.3(約定 DNS,helper 的 DNS pivot)讓 /proc/net/pnp 有 nameserver 行。
assert!(cmdline.contains("ip=10.0.2.15::10.0.2.2:255.255.255.0::eth0:off:10.0.2.3"));
assert!(!cmdline.contains("ip=dhcp"));
assert!(cmdline.contains("nolapic"));
assert!(cmdline.contains("lpj=1000000"));
Expand Down
2 changes: 2 additions & 0 deletions crates/whp-helper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
# 容器內 DNS(10.0.2.3 pivot):GetNetworkParams 取 host 設定的 DNS server 當轉發上游。
"Win32_NetworkManagement_IpHelper",
# GUI 顯示視窗(M8-c):視窗類別/訊息迴圈、GDI blit(StretchDIBits)。
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
Expand Down
21 changes: 17 additions & 4 deletions crates/whp-helper/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,9 @@ mod whp_api {
// vdb(data)image 容量上限(host RAM 內的 backing;env CHEFER_WHP_DATA_MIB 覆寫,預設 256MiB)。
// 必須 ≥ guest 關機 re-tar data_dir 後的 tar 大小,否則回寫 IOERR。
const VIRTIO_DATA_DEFAULT_MIB: u64 = 256;
// user-mode 網路:smoltcp gateway 10.0.2.2、guest 靜態 10.0.2.15/24(與 appliance init 約定)。
// user-mode 網路:smoltcp gateway 10.0.2.2、guest 靜態 10.0.2.15/24(與 appliance init 約定);
// 約定 DNS 10.0.2.3:53(net_backend::NAT_DNS_IP,kernel cmdline dns0 指向它,
// drain_tx 分流到 DNS pivot 轉發 host 設定的 DNS)。
const NET_GATEWAY_MAC: [u8; 6] = [0x52, 0x54, 0x00, 0x00, 0x00, 0x02];
const NET_GATEWAY_IP: [u8; 4] = [10, 0, 2, 2];
const NET_GUEST_MAC: [u8; 6] = [0x52, 0x54, 0x00, 0x12, 0x34, 0x56];
Expand Down Expand Up @@ -1105,17 +1107,28 @@ mod whp_api {
);
}
// 出網分流:
// - 約定 DNS `10.0.2.3:53`:UDP → DNS pivot(轉發 host 設定的 DNS
// 上游、回程 masquerade);TCP SYN → 同 pivot 的 TCP fallback
// (截斷回應時 resolver 改走 TCP:53)。須在 is_external_dst 之前
// 分流——10.0.2.3 在 guest 網段內、不算外部。
// - 外部 dst 的 TCP:仍交給 smoltcp(出網 TCP 由 smoltcp 動態 dst 處理);
// 但 SYN 先 nat_tcp_syn 預註冊 dst+listen socket+背景 connect host。
// - 外部 dst 的 UDP:手工 NAT(不交 smoltcp,smoltcp 不轉發)。
// - 其餘(ARP、guest↔gateway、同網段、multicast/bcast)照舊餵 smoltcp。
use super::virtio::net_backend::NAT_DNS_IP;
if let Some(t) = super::virtio::nat::parse_tcp(&frame) {
if is_external_dst(t.dst_ip) && t.syn && !t.ack {
self.backend.nat_tcp_syn(t);
if t.syn && !t.ack {
if t.dst_ip == NAT_DNS_IP && t.dst_port == 53 {
self.backend.nat_tcp_dns_syn(t);
} else if is_external_dst(t.dst_ip) {
self.backend.nat_tcp_syn(t);
}
}
self.phy.push_from_guest(frame);
} else if let Some(udp) = super::virtio::nat::parse_udp(&frame) {
if is_external_dst(udp.dst_ip) {
if udp.dst_ip == NAT_DNS_IP && udp.dst_port == 53 {
self.backend.nat_outbound_dns(udp);
} else if is_external_dst(udp.dst_ip) {
self.backend.nat_outbound(udp);
} else {
self.phy.push_from_guest(frame);
Expand Down
Loading
Loading