diff --git a/Cargo.lock b/Cargo.lock index a0ff756..e4829d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,7 +119,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 2.0.61", ] [[package]] @@ -189,6 +189,78 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "const-str" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "either" version = "1.11.0" @@ -233,6 +305,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "futures-core" version = "0.3.30" @@ -256,6 +334,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getset" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.61", +] + [[package]] name = "gimli" version = "0.29.0" @@ -286,6 +376,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "input-linux" version = "0.7.1" @@ -379,6 +475,16 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.21" @@ -433,11 +539,13 @@ dependencies = [ "anyhow", "bpaf", "byteorder", + "const-str", "env_logger", "input-linux", "input-linux-sys", "krun-sys", "log", + "neli", "nix", "procfs", "rustix", @@ -450,6 +558,35 @@ dependencies = [ "uuid", ] +[[package]] +name = "neli" +version = "0.7.0-rc2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de016d5ec13f40df05f92660c9322325d7e15e3ea6f9b365a6e0ecc7cb0b7f2" +dependencies = [ + "bitflags", + "byteorder", + "derive_builder", + "getset", + "libc", + "log", + "neli-proc-macros", + "parking_lot", +] + +[[package]] +name = "neli-proc-macros" +version = "0.2.0-rc2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce2ca79a37046e8897da8e967f22d7a6c76b9d5ffafbc8d7c57185a63ffe9ff" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + [[package]] name = "nix" version = "0.29.0" @@ -492,6 +629,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -504,6 +664,28 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.61", +] + [[package]] name = "proc-macro2" version = "1.0.82" @@ -544,6 +726,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.10.4" @@ -604,6 +795,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.203" @@ -621,7 +818,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.61", ] [[package]] @@ -650,6 +847,12 @@ dependencies = [ "libc", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "socket2" version = "0.5.7" @@ -660,6 +863,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.61" @@ -709,7 +929,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.61", ] [[package]] diff --git a/crates/muvm/Cargo.toml b/crates/muvm/Cargo.toml index d5da639..72bd8b1 100644 --- a/crates/muvm/Cargo.toml +++ b/crates/muvm/Cargo.toml @@ -3,7 +3,7 @@ name = "muvm" version = "0.1.3" authors = ["Sergio Lopez ", "Teoh Han Hui ", "Sasha Finkelstein ", "Asahi Lina "] edition = "2021" -rust-version = "1.77.0" +rust-version = "1.80.0" description = "Run programs from your system in a microVM" repository = "https://github.com/AsahiLinux/muvm" license = "MIT" @@ -12,11 +12,13 @@ license = "MIT" anyhow = { version = "1.0.82", default-features = false, features = ["std"] } bpaf = { version = "0.9.11", default-features = false, features = [] } byteorder = { version = "1.5.0", default-features = false, features = ["std"] } +const-str = { version = "0.5.7", default-features = false, features = [] } env_logger = { version = "0.11.3", default-features = false, features = ["auto-color", "humantime", "unstable-kv"] } input-linux = { version = "0.7.0", default-features = false, features = [] } input-linux-sys = { version = "0.9.0", default-features = false, features = [] } krun-sys = { path = "../krun-sys", version = "1.9.1", default-features = false, features = [] } log = { version = "0.4.21", default-features = false, features = ["kv"] } +neli = { version = "0.7.0-rc2", default-features = false, features = ["sync"] } nix = { version = "0.29.0", default-features = false, features = ["user"] } procfs = { version = "0.17.0", default-features = false, features = [] } rustix = { version = "0.38.34", default-features = false, features = ["fs", "mount", "process", "std", "stdio", "system", "use-libc-auxv"] } diff --git a/crates/muvm/src/guest/net.rs b/crates/muvm/src/guest/net.rs index b47f2e1..dd83c6a 100644 --- a/crates/muvm/src/guest/net.rs +++ b/crates/muvm/src/guest/net.rs @@ -1,14 +1,239 @@ use std::fs; use std::io::Write; -use std::os::unix::process::ExitStatusExt as _; -use std::process::Command; +use std::net::{Ipv4Addr, UdpSocket}; +use std::time::Duration; -use anyhow::{anyhow, Context, Result}; -use log::debug; +use anyhow::{Context, Result}; +use neli::consts::nl::NlmF; +use neli::consts::rtnl::{ + Arphrd, Ifa, IfaF, Iff, RtAddrFamily, RtScope, RtTable, Rta, Rtm, RtmF, Rtn, Rtprot, +}; +use neli::consts::socket::NlFamily; +use neli::nl::{NlPayload, Nlmsghdr}; +use neli::router::synchronous::{NlRouter, NlRouterReceiverHandle}; +use neli::rtnl::{ + Ifaddrmsg, IfaddrmsgBuilder, Ifinfomsg, IfinfomsgBuilder, RtattrBuilder, Rtmsg, RtmsgBuilder, +}; +use neli::types::RtBuffer; +use neli::utils::Groups; use rustix::system::sethostname; -use crate::utils::env::find_in_path; -use crate::utils::fs::find_executable; +/// Set interface flags for eth0 (interface index 2) with a given mask +fn flags_eth0(rtnl: &NlRouter, mask: Iff, set: Iff) -> Result<()> { + let ifinfomsg = IfinfomsgBuilder::default() + .ifi_family(RtAddrFamily::Unspecified) + .ifi_type(Arphrd::Ether) + .ifi_index(2) + .ifi_change(mask) + .ifi_flags(set) + .build()?; + + let _: NlRouterReceiverHandle = + rtnl.send(Rtm::Newlink, NlmF::REQUEST, NlPayload::Payload(ifinfomsg))?; + + Ok(()) +} + +/// Add or delete IPv4 routes for eth0 (interface index 2) +fn route4_eth0(rtnl: &NlRouter, what: Rtm, gw: Ipv4Addr) -> Result<()> { + let rtmsg = RtmsgBuilder::default() + .rtm_family(RtAddrFamily::Inet) + .rtm_dst_len(0) + .rtm_src_len(0) + .rtm_tos(0) + .rtm_table(RtTable::Main) + .rtm_protocol(Rtprot::Boot) + .rtm_scope(RtScope::Universe) + .rtm_type(Rtn::Unicast) + .rtm_flags(RtmF::empty()) + .rtattrs(RtBuffer::from_iter([ + RtattrBuilder::default() + .rta_type(Rta::Oif) + .rta_payload(2) + .build()?, + RtattrBuilder::default() + .rta_type(Rta::Dst) + .rta_payload(Ipv4Addr::UNSPECIFIED.octets().to_vec()) + .build()?, + RtattrBuilder::default() + .rta_type(Rta::Gateway) + .rta_payload(gw.octets().to_vec()) + .build()?, + ])) + .build()?; + + let _: NlRouterReceiverHandle = rtnl.send( + what, + NlmF::CREATE | NlmF::REQUEST, + NlPayload::Payload(rtmsg), + )?; + + Ok(()) +} + +/// Add or delete IPv4 addresses for eth0 (interface index 2) +fn addr4_eth0(rtnl: &NlRouter, what: Rtm, addr: Ipv4Addr, prefix_len: u8) -> Result<()> { + let ifaddrmsg = IfaddrmsgBuilder::default() + .ifa_family(RtAddrFamily::Inet) + .ifa_prefixlen(prefix_len) + .ifa_scope(RtScope::Universe) + .ifa_index(2) + .rtattrs(RtBuffer::from_iter([ + RtattrBuilder::default() + .rta_type(Ifa::Local) + .rta_payload(addr.octets().to_vec()) + .build()?, + RtattrBuilder::default() + .rta_type(Ifa::Address) + .rta_payload(addr.octets().to_vec()) + .build()?, + ])) + .build()?; + + let _: NlRouterReceiverHandle = rtnl.send( + what, + NlmF::CREATE | NlmF::REQUEST, + NlPayload::Payload(ifaddrmsg), + )?; + + Ok(()) +} + +/// Send DISCOVER with Rapid Commit, process ACK, configure address and route +fn do_dhcp(rtnl: &NlRouter) -> Result<()> { + // Temporary link-local address and route avoid the need for raw sockets + route4_eth0(rtnl, Rtm::Newroute, Ipv4Addr::UNSPECIFIED)?; + addr4_eth0(rtnl, Rtm::Newaddr, Ipv4Addr::new(169, 254, 1, 1), 16)?; + + // Send request (DHCPDISCOVER) + let socket = UdpSocket::bind("0.0.0.0:68").expect("Failed to bind"); + let mut buf = [0; 576 /* RFC 2131, Section 2 */ ]; + + const REQUEST: &[u8; 300 /* From RFC 951: >= 60 B of options */ ] = const_str::concat_bytes!( + 1, // BOOTREQUEST + 0x01, // Hardware address type: Ethernet + 6, // Hardware address length + 0, // DHCP relay Hops + [1, 2, 3, 4], // Transaction ID: we're the only client, use a fixed one + [0, 0], // Seconds elapsed since beginning of acquisition or renewal + [0x80, 0x0], // DHCP message flags: Broadcast + [0; 16], // All-zero ciaddr, yiaddr, siaddr, giaddr + [0; 16], // Client hardware address, not set as we're the only client + [0; 64], // Server host name + [0; 128], // Boot file name + [0x63, 0x82, 0x53, 0x63], // Magic cookie prefix before options + // Options + [ + 53, 1, 1, // DHCPDISCOVER + 80, 0, // Rapid Commit (RFC 4039) + ], + 0xff, // End of options + [0; 54], // Pad (up to 300 bytes) + ); + + socket.set_broadcast(true)?; + socket.send_to(REQUEST, "255.255.255.255:67")?; + + // Keep IPv6-only fast + let _ = socket.set_read_timeout(Some(Duration::from_millis(100))); + + // Get and process response (DHCPACK) if any + if let Ok((len, _)) = socket.recv_from(&mut buf) { + let msg = &mut buf[..len]; + + let addr = Ipv4Addr::new(msg[16], msg[17], msg[18], msg[19]); + let mut netmask = Ipv4Addr::UNSPECIFIED; + let mut router = Ipv4Addr::UNSPECIFIED; + let mut p: usize = 240; + let mut resolv = fs::File::options() + .append(true) + .open("/etc/resolv.conf") + .context("Failed to open /etc/resolv.conf")?; + + while p < len { + let o = msg[p]; + let l: u8 = msg[p + 1]; + p += 2; // Length doesn't include code and length field itself + + if o == 1 { + // Option 1: Subnet Mask + netmask = Ipv4Addr::new(msg[p], msg[p + 1], msg[p + 1], msg[p + 3]); + } else if o == 3 { + // Option 3: Router + router = Ipv4Addr::new(msg[p], msg[p + 1], msg[p + 2], msg[p + 3]); + } else if o == 6 { + // Option 6: Domain Name Server + for dns_p in (p..p + l as usize).step_by(4) { + let dns = + Ipv4Addr::new(msg[dns_p], msg[dns_p + 1], msg[dns_p + 2], msg[dns_p + 3]); + resolv + .write_all(format!("nameserver {}\n", dns).as_bytes()) + .context("Failed to write to resolv.conf")?; + } + } else if o == 0xff { + // Option 255: End (of options) + break; + } + + p += l as usize; + } + + let prefix_len: u8 = netmask.to_bits().leading_ones() as u8; + + // Drop temporary address and route, configure what we got instead + route4_eth0(rtnl, Rtm::Delroute, Ipv4Addr::UNSPECIFIED)?; + addr4_eth0(rtnl, Rtm::Deladdr, Ipv4Addr::new(169, 254, 1, 1), 16)?; + + addr4_eth0(rtnl, Rtm::Newaddr, addr, prefix_len)?; + route4_eth0(rtnl, Rtm::Newroute, router)?; + } else { + // Clean up: we're clearly too cool for IPv4 + route4_eth0(rtnl, Rtm::Delroute, Ipv4Addr::UNSPECIFIED)?; + addr4_eth0(rtnl, Rtm::Deladdr, Ipv4Addr::new(169, 254, 1, 1), 16)?; + } + + Ok(()) +} + +/// Wait for SLAAC to complete or fail +fn wait_for_slaac(rtnl: &NlRouter) -> Result<()> { + let mut global_seen = false; + let mut global_wait = true; + let mut ll_seen = false; + + // Busy-netlink-loop until we see a link-local address, and a global unicast + // address as long as we might expect one (see below) + while !ll_seen || (global_wait && !global_seen) { + let ifaddrmsg = IfaddrmsgBuilder::default() + .ifa_family(RtAddrFamily::Inet6) + .ifa_prefixlen(0) + .ifa_scope(RtScope::Universe) + .ifa_index(2) + .build()?; + + let recv = rtnl.send(Rtm::Getaddr, NlmF::ROOT, NlPayload::Payload(ifaddrmsg))?; + + for response in recv { + let header: Nlmsghdr = response?; + if let NlPayload::Payload(p) = header.nl_payload() { + if p.ifa_scope() == &RtScope::Link { + // A non-tentative link-local address implies we sent a + // router solicitation that didn't get any response + // (IPv4-only)? Stop waiting for the router in that case + if *p.ifa_flags() & IfaF::TENTATIVE != IfaF::TENTATIVE { + global_wait = false; + } + + ll_seen = true; + } else if p.ifa_scope() == &RtScope::Universe { + global_seen = true; + } + } + } + } + + Ok(()) +} pub fn configure_network() -> Result<()> { // Allow unprivileged users to use ping, as most distros do by default. @@ -33,63 +258,29 @@ pub fn configure_network() -> Result<()> { sethostname(hostname.as_bytes()).context("Failed to set hostname")?; } - let dhcpcd_path = find_in_path("dhcpcd").context("Failed to check existence of `dhcpcd`")?; - let dhcpcd_path = if let Some(dhcpcd_path) = dhcpcd_path { - Some(dhcpcd_path) - } else { - find_executable("/sbin/dhcpcd").context("Failed to check existence of `/sbin/dhcpcd`")? - }; - if let Some(dhcpcd_path) = dhcpcd_path { - let output = Command::new(dhcpcd_path) - .args(["-M", "--nodev", "eth0"]) - .output() - .context("Failed to execute `dhcpcd` as child process")?; - debug!(output:?; "dhcpcd output"); - if !output.status.success() { - let err = if let Some(code) = output.status.code() { - anyhow!("`dhcpcd` process exited with status code: {code}") - } else { - anyhow!( - "`dhcpcd` process terminated by signal: {}", - output - .status - .signal() - .expect("either one of status code or signal should be set") - ) - }; - Err(err)?; - } + let (rtnl, _) = NlRouter::connect(NlFamily::Route, None, Groups::empty())?; + rtnl.enable_strict_checking(true)?; - return Ok(()); + // Disable neighbour solicitations (dodge DAD), bring up link to start SLAAC + { + // IFF_NOARP | IFF_UP in one shot delays router solicitations, avoid it + flags_eth0(&rtnl, Iff::NOARP, Iff::NOARP)?; + flags_eth0(&rtnl, Iff::UP, Iff::UP)?; } - let dhclient_path = - find_in_path("dhclient").context("Failed to check existence of `dhclient`")?; - let dhclient_path = if let Some(dhclient_path) = dhclient_path { - Some(dhclient_path) - } else { - find_executable("/sbin/dhclient") - .context("Failed to check existence of `/sbin/dhclient`")? - }; - let dhclient_path = - dhclient_path.ok_or_else(|| anyhow!("could not find required `dhcpcd` or `dhclient`"))?; - let output = Command::new(dhclient_path) - .output() - .context("Failed to execute `dhclient` as child process")?; - debug!(output:?; "dhclient output"); - if !output.status.success() { - let err = if let Some(code) = output.status.code() { - anyhow!("`dhclient` process exited with status code: {code}") - } else { - anyhow!( - "`dhclient` process terminated by signal: {}", - output - .status - .signal() - .expect("either one of status code or signal should be set") - ) - }; - Err(err)?; + // Configure IPv4 + { + do_dhcp(&rtnl)?; + } + + // Ensure IPv6 setup is done, if available + { + wait_for_slaac(&rtnl)?; + } + + // Re-enable neighbour solicitations and ARP requests + { + flags_eth0(&rtnl, Iff::NOARP, Iff::empty())?; } Ok(())