Skip to content

Commit bd1f9e9

Browse files
committed
guest/net: New implementation of network setup with SLAAC and own DHCP client
The existing implementation has a couple of issues: - it doesn't support IPv6 or SLAAC - it relies on either dhclient(8) or dhcpcd(8), which need a significant amount of time to configure the network as they are rather generic DHCP clients - on top of this, dhcpcd, by default, unless --noarp is given, will spend five seconds ARP-probing the address it just received before configuring it Replace the IPv4 part with a minimalistic, 73-line DHCP client that just does what we need, using option 80 (Rapid Commit) to speed up the whole exchange. Add IPv6 support (including IPv4-only, and IPv6-only modes) relying on the kernel to perform SLAAC. Safely avoid DAD (we're the only node on the link) by disabling router solicitations, starting SLAAC, and re-enabling them once addresses are configured. Instead of merely triggering the network setup and proceeding, wait until everything is configured, so that connectivity is guaranteed to be ready before any further process runs in the guest, say: $ ./target/debug/muvm -- ping -c1 2a01:4f8:222:904::2 PING 2a01:4f8:222:904::2 (2a01:4f8:222:904::2) 56 data bytes 64 bytes from 2a01:4f8:222:904::2: icmp_seq=1 ttl=255 time=0.256 ms --- 2a01:4f8:222:904::2 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.256/0.256/0.256/0.000 ms The whole procedure now takes approximately 1.5 to 2 ms (for both IPv4 and IPv6), with the DHCP exchange and configuration taking somewhere around 300-500 µs out of that, instead of hundreds of milliseconds to seconds. Matching support in passt for option 80 (RFC 4039) and for the DHCP "broadcast" flag (RFC 2131) needs at least passt 2024_11_27.c0fbc7e: https://archives.passt.top/passt-user/20241127142126.3c53066e@elisabeth/ Signed-off-by: Stefano Brivio <[email protected]>
1 parent 8b49072 commit bd1f9e9

2 files changed

Lines changed: 229 additions & 61 deletions

File tree

crates/muvm/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "muvm"
33
version = "0.1.3"
44
authors = ["Sergio Lopez <[email protected]>", "Teoh Han Hui <[email protected]>", "Sasha Finkelstein <[email protected]>", "Asahi Lina <[email protected]>"]
55
edition = "2021"
6-
rust-version = "1.77.0"
6+
rust-version = "1.80.0"
77
description = "Run programs from your system in a microVM"
88
repository = "https://github.com/AsahiLinux/muvm"
99
license = "MIT"
@@ -15,6 +15,7 @@ byteorder = { version = "1.5.0", default-features = false, features = ["std"] }
1515
env_logger = { version = "0.11.3", default-features = false, features = ["auto-color", "humantime", "unstable-kv"] }
1616
krun-sys = { path = "../krun-sys", version = "1.9.1", default-features = false, features = [] }
1717
log = { version = "0.4.21", default-features = false, features = ["kv"] }
18+
neli = { version = "0.7.0-rc2", default-features = false, features = ["sync"] }
1819
nix = { version = "0.29.0", default-features = false, features = ["user"] }
1920
procfs = { version = "0.17.0", default-features = false, features = [] }
2021
rustix = { version = "0.38.34", default-features = false, features = ["fs", "mount", "process", "std", "stdio", "system", "use-libc-auxv"] }

crates/muvm/src/guest/net.rs

Lines changed: 227 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,215 @@
11
use std::fs;
22
use std::io::Write;
3-
use std::os::unix::process::ExitStatusExt as _;
4-
use std::process::Command;
3+
use std::net::{UdpSocket, Ipv4Addr};
4+
use std::time::Duration;
55

6-
use anyhow::{anyhow, Context, Result};
7-
use log::debug;
6+
use anyhow::{Context, Result};
87
use rustix::system::sethostname;
98

10-
use crate::utils::env::find_in_path;
11-
use crate::utils::fs::find_executable;
9+
use neli::{
10+
consts::{
11+
nl::NlmF,
12+
rtnl::{Arphrd, Ifa, IfaF, Iff, Rta, RtAddrFamily, Rtm, RtmF, Rtn,
13+
Rtprot, RtScope, RtTable},
14+
socket::NlFamily,
15+
},
16+
nl::{NlPayload, Nlmsghdr},
17+
router::synchronous::{NlRouter, NlRouterReceiverHandle},
18+
rtnl::{Ifaddrmsg, IfaddrmsgBuilder, Ifinfomsg, IfinfomsgBuilder,
19+
RtattrBuilder, Rtmsg, RtmsgBuilder},
20+
utils::Groups,
21+
types::RtBuffer,
22+
};
23+
24+
/// Set interface flags for eth0 (interface index 2) with a given mask
25+
fn flags_eth0(rtnl: &NlRouter, mask: Iff, set: Iff) -> Result<()> {
26+
let ifinfomsg = IfinfomsgBuilder::default()
27+
.ifi_family(RtAddrFamily::Unspecified)
28+
.ifi_type(Arphrd::Ether).ifi_index(2)
29+
.ifi_change(mask).ifi_flags(set)
30+
.build()?;
31+
32+
let _: NlRouterReceiverHandle<Rtm, Ifinfomsg> =
33+
rtnl.send(Rtm::Newlink, NlmF::REQUEST, NlPayload::Payload(ifinfomsg))?;
34+
35+
Ok(())
36+
}
37+
38+
/// Add or delete IPv4 routes for eth0 (interface index 2)
39+
fn route4_eth0(rtnl: &NlRouter, what: Rtm, gw: Ipv4Addr) -> Result<()> {
40+
let rtmsg = RtmsgBuilder::default()
41+
.rtm_family(RtAddrFamily::Inet)
42+
.rtm_dst_len(0).rtm_src_len(0).rtm_tos(0)
43+
.rtm_table(RtTable::Main).rtm_protocol(Rtprot::Boot)
44+
.rtm_scope(RtScope::Universe).rtm_type(Rtn::Unicast)
45+
.rtm_flags(RtmF::empty())
46+
.rtattrs(RtBuffer::from_iter([
47+
RtattrBuilder::default()
48+
.rta_type(Rta::Oif)
49+
.rta_payload(2)
50+
.build()?,
51+
RtattrBuilder::default()
52+
.rta_type(Rta::Dst)
53+
.rta_payload(Ipv4Addr::UNSPECIFIED.octets().to_vec())
54+
.build()?,
55+
RtattrBuilder::default()
56+
.rta_type(Rta::Gateway)
57+
.rta_payload(gw.octets().to_vec())
58+
.build()?
59+
]))
60+
.build()?;
61+
62+
let _: NlRouterReceiverHandle<Rtm, Rtmsg> =
63+
rtnl.send(what, NlmF::CREATE | NlmF::REQUEST,
64+
NlPayload::Payload(rtmsg))?;
65+
66+
Ok(())
67+
}
68+
69+
/// Add or delete IPv4 addresses for eth0 (interface index 2)
70+
fn addr4_eth0(rtnl: &NlRouter, what: Rtm, addr: Ipv4Addr, prefix_len: u8)
71+
-> Result<()> {
72+
let ifaddrmsg = IfaddrmsgBuilder::default()
73+
.ifa_family(RtAddrFamily::Inet)
74+
.ifa_prefixlen(prefix_len)
75+
.ifa_scope(RtScope::Universe)
76+
.ifa_index(2)
77+
.rtattrs(RtBuffer::from_iter([
78+
RtattrBuilder::default()
79+
.rta_type(Ifa::Local)
80+
.rta_payload(addr.octets().to_vec())
81+
.build()?,
82+
RtattrBuilder::default()
83+
.rta_type(Ifa::Address)
84+
.rta_payload(addr.octets().to_vec())
85+
.build()?,
86+
]))
87+
.build()?;
88+
89+
let _: NlRouterReceiverHandle<Rtm, Ifaddrmsg> =
90+
rtnl.send(what, NlmF::CREATE | NlmF::REQUEST,
91+
NlPayload::Payload(ifaddrmsg))?;
92+
93+
Ok(())
94+
}
95+
96+
/// Send DISCOVER with Rapid Commit, process ACK, configure address and route
97+
fn do_dhcp(rtnl: &NlRouter) -> Result<()> {
98+
// Temporary link-local address and route avoid the need for raw sockets
99+
route4_eth0(rtnl, Rtm::Newroute, Ipv4Addr::UNSPECIFIED)?;
100+
addr4_eth0(rtnl, Rtm::Newaddr, Ipv4Addr::new(169, 254, 1, 1), 16)?;
101+
102+
// Send request (DHCPDISCOVER)
103+
let socket = UdpSocket::bind("0.0.0.0:68").expect("Failed to bind");
104+
let mut buf = [0; 576 /* RFC 2131, Section 2 */ ];
105+
106+
const REQUEST: [u8; 300 /* From RFC 951: >= 60 B of options */ ] = [
107+
1 /* REQUEST */, 0x1 /* Ethernet */, 6 /* hlen */, 0 /* Hops */,
108+
1, 2, 3, 4 /* XID */, 0, 0 /* Seconds */, 0x80, 0x0 /* Flags */,
109+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* All-zero (four) addresses */
110+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 16B HW address: who cares */
111+
/* 32 bytes per row: 64B 'sname', plus 128B 'file' (RFC 1531) */
112+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
113+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
114+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
115+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
116+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
117+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
118+
0x63, 0x82, 0x53, 0x63, /* DHCP (magic) cookie, then options: */
119+
53, 1, 1 /* DISCOVER */, 80, 0 /* Rapid commit */, 0xff, // Done
120+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
121+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 /* 54B paaaadding */
122+
];
123+
124+
socket.set_broadcast(true)?;
125+
socket.send_to(&REQUEST, "255.255.255.255:67")?;
126+
127+
// Keep IPv6-only fast
128+
let _ = socket.set_read_timeout(Some(Duration::from_millis(100)));
129+
130+
// Get and process response (DHCPACK) if any
131+
if let Ok((len, _)) = socket.recv_from(&mut buf) {
132+
let msg = &mut buf[..len];
133+
134+
let addr = Ipv4Addr::new(msg[16], msg[17], msg[18], msg[19]);
135+
let mut netmask = Ipv4Addr::UNSPECIFIED;
136+
let mut router = Ipv4Addr::UNSPECIFIED;
137+
let mut p: usize = 240;
138+
139+
while p < len {
140+
let o = msg[p];
141+
let mut l: u8;
142+
l = msg[p + 1];
143+
144+
if o == 1 { // Option 1: Subnet Mask
145+
netmask = Ipv4Addr::new(msg[p + 2], msg[p + 3],
146+
msg[p + 4], msg[p + 5]);
147+
} else if o == 3 { // Option 3: Router
148+
router = Ipv4Addr::new(msg[p + 2], msg[p + 3],
149+
msg[p + 4], msg[p + 5]);
150+
} else if o == 0xff { // Option 255: End (of options)
151+
break;
152+
}
153+
154+
l += 2; // Length doesn't include code and length field itself
155+
p += l as usize;
156+
}
157+
158+
let prefix_len : u8 = netmask.to_bits().leading_ones() as u8;
159+
160+
// Drop temporary address and route, configure what we got instead
161+
route4_eth0(rtnl, Rtm::Delroute, Ipv4Addr::UNSPECIFIED)?;
162+
addr4_eth0(rtnl, Rtm::Deladdr, Ipv4Addr::new(169, 254, 1, 1), 16)?;
163+
164+
addr4_eth0(rtnl, Rtm::Newaddr, addr, prefix_len)?;
165+
route4_eth0(rtnl, Rtm::Newroute, router)?;
166+
} else {
167+
// Clean up: we're clearly too cool for IPv4
168+
route4_eth0(rtnl, Rtm::Delroute, Ipv4Addr::UNSPECIFIED)?;
169+
addr4_eth0(rtnl, Rtm::Deladdr, Ipv4Addr::new(169, 254, 1, 1), 16)?;
170+
}
171+
172+
Ok(())
173+
}
174+
175+
/// Wait for SLAAC to complete or fail
176+
fn wait_for_slaac(rtnl: &NlRouter) -> Result<()> {
177+
let mut global_seen = false;
178+
let mut global_wait = true;
179+
let mut ll_seen = false;
180+
181+
// Busy-netlink-loop until we see a link-local address, and a global unicast
182+
// address as long as we might expect one (see below)
183+
while !ll_seen || (global_wait && !global_seen) {
184+
let ifaddrmsg = IfaddrmsgBuilder::default()
185+
.ifa_family(RtAddrFamily::Inet6)
186+
.ifa_prefixlen(0).ifa_scope(RtScope::Universe).ifa_index(2)
187+
.build()?;
188+
189+
let recv = rtnl.send(Rtm::Getaddr, NlmF::ROOT,
190+
NlPayload::Payload(ifaddrmsg))?;
191+
192+
for response in recv {
193+
let header: Nlmsghdr<Rtm, Ifaddrmsg> = response?;
194+
if let NlPayload::Payload(p) = header.nl_payload() {
195+
if p.ifa_scope() == &RtScope::Link {
196+
// A non-tentative link-local address implies we sent a
197+
// router solicitation that didn't get any response
198+
// (IPv4-only)? Stop waiting for the router in that case
199+
if *p.ifa_flags() & IfaF::TENTATIVE != IfaF::TENTATIVE {
200+
global_wait = false;
201+
}
202+
203+
ll_seen = true;
204+
} else if p.ifa_scope() == &RtScope::Universe {
205+
global_seen = true;
206+
}
207+
}
208+
}
209+
}
210+
211+
Ok(())
212+
}
12213

13214
pub fn configure_network() -> Result<()> {
14215
// Allow unprivileged users to use ping, as most distros do by default.
@@ -33,63 +234,29 @@ pub fn configure_network() -> Result<()> {
33234
sethostname(hostname.as_bytes()).context("Failed to set hostname")?;
34235
}
35236

36-
let dhcpcd_path = find_in_path("dhcpcd").context("Failed to check existence of `dhcpcd`")?;
37-
let dhcpcd_path = if let Some(dhcpcd_path) = dhcpcd_path {
38-
Some(dhcpcd_path)
39-
} else {
40-
find_executable("/sbin/dhcpcd").context("Failed to check existence of `/sbin/dhcpcd`")?
41-
};
42-
if let Some(dhcpcd_path) = dhcpcd_path {
43-
let output = Command::new(dhcpcd_path)
44-
.args(["-M", "--nodev", "eth0"])
45-
.output()
46-
.context("Failed to execute `dhcpcd` as child process")?;
47-
debug!(output:?; "dhcpcd output");
48-
if !output.status.success() {
49-
let err = if let Some(code) = output.status.code() {
50-
anyhow!("`dhcpcd` process exited with status code: {code}")
51-
} else {
52-
anyhow!(
53-
"`dhcpcd` process terminated by signal: {}",
54-
output
55-
.status
56-
.signal()
57-
.expect("either one of status code or signal should be set")
58-
)
59-
};
60-
Err(err)?;
61-
}
237+
let (rtnl, _) = NlRouter::connect(NlFamily::Route, None, Groups::empty())?;
238+
rtnl.enable_strict_checking(true)?;
62239

63-
return Ok(());
240+
// Disable neighbour solicitations (dodge DAD), bring up link to start SLAAC
241+
{
242+
// IFF_NOARP | IFF_UP in one shot delays router solicitations, avoid it
243+
flags_eth0(&rtnl, Iff::NOARP, Iff::NOARP)?;
244+
flags_eth0(&rtnl, Iff::UP, Iff::UP)?;
64245
}
65246

66-
let dhclient_path =
67-
find_in_path("dhclient").context("Failed to check existence of `dhclient`")?;
68-
let dhclient_path = if let Some(dhclient_path) = dhclient_path {
69-
Some(dhclient_path)
70-
} else {
71-
find_executable("/sbin/dhclient")
72-
.context("Failed to check existence of `/sbin/dhclient`")?
73-
};
74-
let dhclient_path =
75-
dhclient_path.ok_or_else(|| anyhow!("could not find required `dhcpcd` or `dhclient`"))?;
76-
let output = Command::new(dhclient_path)
77-
.output()
78-
.context("Failed to execute `dhclient` as child process")?;
79-
debug!(output:?; "dhclient output");
80-
if !output.status.success() {
81-
let err = if let Some(code) = output.status.code() {
82-
anyhow!("`dhclient` process exited with status code: {code}")
83-
} else {
84-
anyhow!(
85-
"`dhclient` process terminated by signal: {}",
86-
output
87-
.status
88-
.signal()
89-
.expect("either one of status code or signal should be set")
90-
)
91-
};
92-
Err(err)?;
247+
// Configure IPv4
248+
{
249+
do_dhcp(&rtnl)?;
250+
}
251+
252+
// Ensure IPv6 setup is done, if available
253+
{
254+
wait_for_slaac(&rtnl)?;
255+
}
256+
257+
// Re-enable neighbour solicitations and ARP requests
258+
{
259+
flags_eth0(&rtnl, Iff::NOARP, Iff::empty())?;
93260
}
94261

95262
Ok(())

0 commit comments

Comments
 (0)