Skip to main content

microsandbox_agentd/
network.rs

1//! Guest-side network configuration from `MSB_NET*` environment variables.
2//!
3//! Configures the guest network interface using ioctls and netlink, following
4//! the parameters provided by the host supervisor via `msbnet`.
5//!
6//! This module only performs real work on Linux. On other platforms, it is a no-op.
7
8#[cfg(target_os = "linux")]
9use std::net::{Ipv4Addr, Ipv6Addr};
10
11#[cfg(target_os = "linux")]
12use crate::error::AgentdError;
13use crate::error::AgentdResult;
14
15//--------------------------------------------------------------------------------------------------
16// Types
17//--------------------------------------------------------------------------------------------------
18
19/// Parsed `MSB_NET` specification.
20#[derive(Debug)]
21#[cfg(target_os = "linux")]
22struct NetSpec<'a> {
23    iface: &'a str,
24    mac: [u8; 6],
25    mtu: u16,
26}
27
28/// Parsed `MSB_NET_IPV4` specification.
29#[derive(Debug)]
30#[cfg(target_os = "linux")]
31struct NetIpv4Spec {
32    address: Ipv4Addr,
33    prefix_len: u8,
34    gateway: Ipv4Addr,
35    dns: Option<Ipv4Addr>,
36}
37
38/// Parsed `MSB_NET_IPV6` specification.
39#[derive(Debug)]
40#[cfg(target_os = "linux")]
41struct NetIpv6Spec {
42    address: Ipv6Addr,
43    prefix_len: u8,
44    gateway: Ipv6Addr,
45    dns: Option<Ipv6Addr>,
46}
47
48//--------------------------------------------------------------------------------------------------
49// Functions
50//--------------------------------------------------------------------------------------------------
51
52/// Applies network configuration from `MSB_NET*` environment variables.
53///
54/// Missing `MSB_NET` is not an error (no networking requested).
55/// Parse failures and configuration failures are hard errors.
56#[cfg(target_os = "linux")]
57pub fn apply_network_config() -> AgentdResult<()> {
58    let val = match std::env::var(microsandbox_protocol::ENV_NET) {
59        Ok(v) if !v.is_empty() => v,
60        _ => return Ok(()),
61    };
62
63    let net = parse_net(&val)?;
64
65    // Parse optional IPv4 config.
66    let ipv4 = match std::env::var(microsandbox_protocol::ENV_NET_IPV4) {
67        Ok(v) if !v.is_empty() => Some(parse_net_ipv4(&v)?),
68        _ => None,
69    };
70
71    // Parse optional IPv6 config.
72    let ipv6 = match std::env::var(microsandbox_protocol::ENV_NET_IPV6) {
73        Ok(v) if !v.is_empty() => Some(parse_net_ipv6(&v)?),
74        _ => None,
75    };
76
77    linux::configure_interface(&net, ipv4.as_ref(), ipv6.as_ref())
78}
79
80/// No-op on non-Linux platforms.
81#[cfg(not(target_os = "linux"))]
82pub fn apply_network_config() -> AgentdResult<()> {
83    Ok(())
84}
85
86#[cfg(target_os = "linux")]
87/// Parses `MSB_NET` value: `iface=NAME,mac=AA:BB:CC:DD:EE:FF,mtu=N`
88fn parse_net(val: &str) -> AgentdResult<NetSpec<'_>> {
89    let mut iface = None;
90    let mut mac = None;
91    let mut mtu = 1500u16;
92
93    for part in val.split(',') {
94        if let Some(v) = part.strip_prefix("iface=") {
95            iface = Some(v);
96        } else if let Some(v) = part.strip_prefix("mac=") {
97            mac = Some(parse_mac(v)?);
98        } else if let Some(v) = part.strip_prefix("mtu=") {
99            mtu = v
100                .parse()
101                .map_err(|_| AgentdError::Init(format!("invalid MTU: {v}")))?;
102        } else {
103            return Err(AgentdError::Init(format!("unknown MSB_NET option: {part}")));
104        }
105    }
106
107    let iface = iface.ok_or_else(|| AgentdError::Init("MSB_NET missing iface=".into()))?;
108    let mac = mac.ok_or_else(|| AgentdError::Init("MSB_NET missing mac=".into()))?;
109
110    Ok(NetSpec { iface, mac, mtu })
111}
112
113#[cfg(target_os = "linux")]
114/// Parses `MSB_NET_IPV4` value: `addr=A.B.C.D/N,gw=A.B.C.D[,dns=A.B.C.D]`
115fn parse_net_ipv4(val: &str) -> AgentdResult<NetIpv4Spec> {
116    let mut address = None;
117    let mut prefix_len = None;
118    let mut gateway = None;
119    let mut dns = None;
120
121    for part in val.split(',') {
122        if let Some(v) = part.strip_prefix("addr=") {
123            let (addr, prefix) = parse_cidr_v4(v)?;
124            address = Some(addr);
125            prefix_len = Some(prefix);
126        } else if let Some(v) = part.strip_prefix("gw=") {
127            gateway = Some(
128                v.parse::<Ipv4Addr>()
129                    .map_err(|_| AgentdError::Init(format!("invalid IPv4 gateway: {v}")))?,
130            );
131        } else if let Some(v) = part.strip_prefix("dns=") {
132            dns = Some(
133                v.parse::<Ipv4Addr>()
134                    .map_err(|_| AgentdError::Init(format!("invalid IPv4 DNS: {v}")))?,
135            );
136        } else {
137            return Err(AgentdError::Init(format!(
138                "unknown MSB_NET_IPV4 option: {part}"
139            )));
140        }
141    }
142
143    let address = address.ok_or_else(|| AgentdError::Init("MSB_NET_IPV4 missing addr=".into()))?;
144    let prefix_len =
145        prefix_len.ok_or_else(|| AgentdError::Init("MSB_NET_IPV4 missing addr=".into()))?;
146    let gateway = gateway.ok_or_else(|| AgentdError::Init("MSB_NET_IPV4 missing gw=".into()))?;
147
148    Ok(NetIpv4Spec {
149        address,
150        prefix_len,
151        gateway,
152        dns,
153    })
154}
155
156#[cfg(target_os = "linux")]
157/// Parses `MSB_NET_IPV6` value: `addr=ADDR/N,gw=ADDR[,dns=ADDR]`
158fn parse_net_ipv6(val: &str) -> AgentdResult<NetIpv6Spec> {
159    let mut address = None;
160    let mut prefix_len = None;
161    let mut gateway = None;
162    let mut dns = None;
163
164    for part in val.split(',') {
165        if let Some(v) = part.strip_prefix("addr=") {
166            let (addr, prefix) = parse_cidr_v6(v)?;
167            address = Some(addr);
168            prefix_len = Some(prefix);
169        } else if let Some(v) = part.strip_prefix("gw=") {
170            gateway = Some(
171                v.parse::<Ipv6Addr>()
172                    .map_err(|_| AgentdError::Init(format!("invalid IPv6 gateway: {v}")))?,
173            );
174        } else if let Some(v) = part.strip_prefix("dns=") {
175            dns = Some(
176                v.parse::<Ipv6Addr>()
177                    .map_err(|_| AgentdError::Init(format!("invalid IPv6 DNS: {v}")))?,
178            );
179        } else {
180            return Err(AgentdError::Init(format!(
181                "unknown MSB_NET_IPV6 option: {part}"
182            )));
183        }
184    }
185
186    let address = address.ok_or_else(|| AgentdError::Init("MSB_NET_IPV6 missing addr=".into()))?;
187    let prefix_len =
188        prefix_len.ok_or_else(|| AgentdError::Init("MSB_NET_IPV6 missing addr=".into()))?;
189    let gateway = gateway.ok_or_else(|| AgentdError::Init("MSB_NET_IPV6 missing gw=".into()))?;
190
191    Ok(NetIpv6Spec {
192        address,
193        prefix_len,
194        gateway,
195        dns,
196    })
197}
198
199#[cfg(target_os = "linux")]
200/// Parses a MAC address string like `02:5a:7b:13:01:02`.
201fn parse_mac(s: &str) -> AgentdResult<[u8; 6]> {
202    let mut mac = [0u8; 6];
203    let mut len = 0usize;
204    for (i, part) in s.split(':').enumerate() {
205        if i >= 6 {
206            return Err(AgentdError::Init(format!("invalid MAC address: {s}")));
207        }
208        mac[i] = u8::from_str_radix(part, 16)
209            .map_err(|_| AgentdError::Init(format!("invalid MAC octet: {part}")))?;
210        len = i + 1;
211    }
212    if len != 6 {
213        return Err(AgentdError::Init(format!("invalid MAC address: {s}")));
214    }
215    Ok(mac)
216}
217
218#[cfg(target_os = "linux")]
219/// Parses an IPv4 CIDR like `100.96.1.2/30`.
220fn parse_cidr_v4(s: &str) -> AgentdResult<(Ipv4Addr, u8)> {
221    let (addr_str, prefix_str) = s
222        .split_once('/')
223        .ok_or_else(|| AgentdError::Init(format!("invalid IPv4 CIDR (missing /): {s}")))?;
224    let addr = addr_str
225        .parse::<Ipv4Addr>()
226        .map_err(|_| AgentdError::Init(format!("invalid IPv4 address: {addr_str}")))?;
227    let prefix = prefix_str
228        .parse::<u8>()
229        .map_err(|_| AgentdError::Init(format!("invalid IPv4 prefix length: {prefix_str}")))?;
230    if prefix > 32 {
231        return Err(AgentdError::Init(format!(
232            "IPv4 prefix length out of range (0-32): {prefix}"
233        )));
234    }
235    Ok((addr, prefix))
236}
237
238#[cfg(target_os = "linux")]
239/// Parses an IPv6 CIDR like `fd42:6d73:62:2a::2/64`.
240fn parse_cidr_v6(s: &str) -> AgentdResult<(Ipv6Addr, u8)> {
241    let (addr_str, prefix_str) = s
242        .rsplit_once('/')
243        .ok_or_else(|| AgentdError::Init(format!("invalid IPv6 CIDR (missing /): {s}")))?;
244    let addr = addr_str
245        .parse::<Ipv6Addr>()
246        .map_err(|_| AgentdError::Init(format!("invalid IPv6 address: {addr_str}")))?;
247    let prefix = prefix_str
248        .parse::<u8>()
249        .map_err(|_| AgentdError::Init(format!("invalid IPv6 prefix length: {prefix_str}")))?;
250    if prefix > 128 {
251        return Err(AgentdError::Init(format!(
252            "IPv6 prefix length out of range (0-128): {prefix}"
253        )));
254    }
255    Ok((addr, prefix))
256}
257
258//--------------------------------------------------------------------------------------------------
259// Modules
260//--------------------------------------------------------------------------------------------------
261
262#[cfg(target_os = "linux")]
263mod linux {
264    use std::net::{Ipv4Addr, Ipv6Addr};
265
266    use crate::error::{AgentdError, AgentdResult};
267
268    use super::{NetIpv4Spec, NetIpv6Spec, NetSpec};
269
270    //----------------------------------------------------------------------------------------------
271    // Types
272    //----------------------------------------------------------------------------------------------
273
274    // Alpine's musl-target libc crate does not expose the Linux netlink
275    // ifaddrmsg/rtmsg definitions, so we define the kernel-layout structs we
276    // need locally and continue using libc only for constants and syscalls.
277    #[repr(C)]
278    struct IfAddrMsg {
279        ifa_family: u8,
280        ifa_prefixlen: u8,
281        ifa_flags: u8,
282        ifa_scope: u8,
283        ifa_index: u32,
284    }
285
286    #[repr(C)]
287    struct RtMsg {
288        rtm_family: u8,
289        rtm_dst_len: u8,
290        rtm_src_len: u8,
291        rtm_tos: u8,
292        rtm_table: u8,
293        rtm_protocol: u8,
294        rtm_scope: u8,
295        rtm_type: u8,
296        rtm_flags: u32,
297    }
298
299    /// Configures the guest network interface using ioctls and netlink.
300    ///
301    /// Operations (in order):
302    /// 1. Set MAC address via `ioctl(SIOCSIFHWADDR)`
303    /// 2. Set MTU via `ioctl(SIOCSIFMTU)`
304    /// 3. Assign IPv4 address via netlink `RTM_NEWADDR`
305    /// 4. Assign IPv6 address via netlink `RTM_NEWADDR`
306    /// 5. Bring interface up via `ioctl(SIOCSIFFLAGS)` with `IFF_UP`
307    /// 6. Add IPv4 default route via netlink `RTM_NEWROUTE`
308    /// 7. Add IPv6 default route via netlink `RTM_NEWROUTE`
309    /// 8. Write `/etc/resolv.conf`
310    pub fn configure_interface(
311        net: &NetSpec<'_>,
312        ipv4: Option<&NetIpv4Spec>,
313        ipv6: Option<&NetIpv6Spec>,
314    ) -> AgentdResult<()> {
315        let ifindex = get_ifindex(net.iface)?;
316
317        set_mac_address(net.iface, &net.mac)?;
318        set_mtu(net.iface, net.mtu)?;
319
320        if let Some(v4) = ipv4 {
321            add_address_v4(ifindex, v4.address, v4.prefix_len)?;
322        }
323        if let Some(v6) = ipv6 {
324            add_address_v6(ifindex, v6.address, v6.prefix_len)?;
325        }
326
327        bring_interface_up(net.iface)?;
328
329        if let Some(v4) = ipv4 {
330            add_default_route_v4(v4.gateway)?;
331        }
332        if let Some(v6) = ipv6 {
333            add_default_route_v6(v6.gateway)?;
334        }
335
336        write_resolv_conf(ipv4.and_then(|v| v.dns), ipv6.and_then(|v| v.dns))?;
337
338        Ok(())
339    }
340
341    // ── ioctl helpers ──────────────────────────────────────────────────
342
343    /// Gets the interface index for a given interface name.
344    fn get_ifindex(ifname: &str) -> AgentdResult<u32> {
345        unsafe {
346            let mut ifr: libc::ifreq = std::mem::zeroed();
347            copy_ifname(&mut ifr, ifname)?;
348
349            let sock = socket_fd()?;
350            if libc::ioctl(sock, libc::SIOCGIFINDEX as _, &mut ifr) < 0 {
351                libc::close(sock);
352                return Err(AgentdError::Init(format!(
353                    "SIOCGIFINDEX failed for {ifname}: {}",
354                    std::io::Error::last_os_error()
355                )));
356            }
357            libc::close(sock);
358
359            Ok(ifr.ifr_ifru.ifru_ifindex as u32)
360        }
361    }
362
363    /// Sets the MAC address on an interface.
364    fn set_mac_address(ifname: &str, mac: &[u8; 6]) -> AgentdResult<()> {
365        unsafe {
366            let mut ifr: libc::ifreq = std::mem::zeroed();
367            copy_ifname(&mut ifr, ifname)?;
368
369            ifr.ifr_ifru.ifru_hwaddr.sa_family = libc::ARPHRD_ETHER;
370            ifr.ifr_ifru.ifru_hwaddr.sa_data[..6].copy_from_slice(&mac.map(|b| b as libc::c_char));
371
372            let sock = socket_fd()?;
373            if libc::ioctl(sock, libc::SIOCSIFHWADDR as _, &ifr) < 0 {
374                libc::close(sock);
375                return Err(AgentdError::Init(format!(
376                    "SIOCSIFHWADDR failed for {ifname}: {}",
377                    std::io::Error::last_os_error()
378                )));
379            }
380            libc::close(sock);
381        }
382        Ok(())
383    }
384
385    /// Sets the MTU on an interface.
386    fn set_mtu(ifname: &str, mtu: u16) -> AgentdResult<()> {
387        unsafe {
388            let mut ifr: libc::ifreq = std::mem::zeroed();
389            copy_ifname(&mut ifr, ifname)?;
390            ifr.ifr_ifru.ifru_mtu = mtu as libc::c_int;
391
392            let sock = socket_fd()?;
393            if libc::ioctl(sock, libc::SIOCSIFMTU as _, &ifr) < 0 {
394                libc::close(sock);
395                return Err(AgentdError::Init(format!(
396                    "SIOCSIFMTU failed for {ifname}: {}",
397                    std::io::Error::last_os_error()
398                )));
399            }
400            libc::close(sock);
401        }
402        Ok(())
403    }
404
405    /// Brings an interface up.
406    fn bring_interface_up(ifname: &str) -> AgentdResult<()> {
407        unsafe {
408            let mut ifr: libc::ifreq = std::mem::zeroed();
409            copy_ifname(&mut ifr, ifname)?;
410
411            let sock = socket_fd()?;
412
413            // Get current flags.
414            if libc::ioctl(sock, libc::SIOCGIFFLAGS as _, &mut ifr) < 0 {
415                libc::close(sock);
416                return Err(AgentdError::Init(format!(
417                    "SIOCGIFFLAGS failed for {ifname}: {}",
418                    std::io::Error::last_os_error()
419                )));
420            }
421
422            // Set IFF_UP.
423            ifr.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short;
424
425            if libc::ioctl(sock, libc::SIOCSIFFLAGS as _, &ifr) < 0 {
426                libc::close(sock);
427                return Err(AgentdError::Init(format!(
428                    "SIOCSIFFLAGS (UP) failed for {ifname}: {}",
429                    std::io::Error::last_os_error()
430                )));
431            }
432            libc::close(sock);
433        }
434        Ok(())
435    }
436
437    // ── netlink helpers ────────────────────────────────────────────────
438
439    /// Adds an IPv4 address to an interface via netlink RTM_NEWADDR.
440    fn add_address_v4(ifindex: u32, addr: Ipv4Addr, prefix_len: u8) -> AgentdResult<()> {
441        let addr_bytes = addr.octets();
442        netlink_newaddr(ifindex, libc::AF_INET as u8, prefix_len, &addr_bytes).map_err(|e| {
443            AgentdError::Init(format!(
444                "failed to add IPv4 address {addr}/{prefix_len}: {e}"
445            ))
446        })
447    }
448
449    /// Adds an IPv6 address to an interface via netlink RTM_NEWADDR.
450    fn add_address_v6(ifindex: u32, addr: Ipv6Addr, prefix_len: u8) -> AgentdResult<()> {
451        let addr_bytes = addr.octets();
452        netlink_newaddr(ifindex, libc::AF_INET6 as u8, prefix_len, &addr_bytes).map_err(|e| {
453            AgentdError::Init(format!(
454                "failed to add IPv6 address {addr}/{prefix_len}: {e}"
455            ))
456        })
457    }
458
459    /// Adds an IPv4 default route via netlink RTM_NEWROUTE.
460    fn add_default_route_v4(gateway: Ipv4Addr) -> AgentdResult<()> {
461        let gw_bytes = gateway.octets();
462        netlink_newroute(libc::AF_INET as u8, &gw_bytes).map_err(|e| {
463            AgentdError::Init(format!(
464                "failed to add IPv4 default route via {gateway}: {e}"
465            ))
466        })
467    }
468
469    /// Adds an IPv6 default route via netlink RTM_NEWROUTE.
470    fn add_default_route_v6(gateway: Ipv6Addr) -> AgentdResult<()> {
471        let gw_bytes = gateway.octets();
472        netlink_newroute(libc::AF_INET6 as u8, &gw_bytes).map_err(|e| {
473            AgentdError::Init(format!(
474                "failed to add IPv6 default route via {gateway}: {e}"
475            ))
476        })
477    }
478
479    /// Sends a netlink RTM_NEWADDR message.
480    ///
481    /// For IPv4: emits both `IFA_ADDRESS` and `IFA_LOCAL` (kernel expects both).
482    /// For IPv6: emits only `IFA_ADDRESS` (no `IFA_LOCAL` semantics for IPv6).
483    fn netlink_newaddr(
484        ifindex: u32,
485        family: u8,
486        prefix_len: u8,
487        addr: &[u8],
488    ) -> std::io::Result<()> {
489        let addr_len = addr.len();
490        let is_ipv4 = family == libc::AF_INET as u8;
491
492        // IPv4 needs two RTAs (IFA_ADDRESS + IFA_LOCAL), IPv6 needs one (IFA_ADDRESS).
493        let num_rtas = if is_ipv4 { 2 } else { 1 };
494        let rtas_len = rta_space(addr_len) * num_rtas;
495        let msg_len = NLMSG_HDRLEN + IFADDRMSG_LEN + rtas_len;
496        let mut buf = vec![0u8; nlmsg_align(msg_len)];
497
498        // nlmsghdr
499        let nlh = buf.as_mut_ptr().cast::<libc::nlmsghdr>();
500        unsafe {
501            (*nlh).nlmsg_len = msg_len as u32;
502            (*nlh).nlmsg_type = libc::RTM_NEWADDR;
503            (*nlh).nlmsg_flags =
504                (libc::NLM_F_REQUEST | libc::NLM_F_ACK | libc::NLM_F_CREATE | libc::NLM_F_EXCL)
505                    as u16;
506            (*nlh).nlmsg_seq = 1;
507        }
508
509        // ifaddrmsg
510        let ifa = unsafe { buf.as_mut_ptr().add(NLMSG_HDRLEN).cast::<IfAddrMsg>() };
511        unsafe {
512            (*ifa).ifa_family = family;
513            (*ifa).ifa_prefixlen = prefix_len;
514            (*ifa).ifa_flags = 0;
515            (*ifa).ifa_index = ifindex;
516            (*ifa).ifa_scope = libc::RT_SCOPE_UNIVERSE;
517        }
518
519        // RTA attributes
520        let mut rta_offset = NLMSG_HDRLEN + IFADDRMSG_LEN;
521        write_rta(&mut buf[rta_offset..], libc::IFA_ADDRESS, addr);
522        rta_offset += rta_space(addr_len);
523
524        if is_ipv4 {
525            write_rta(&mut buf[rta_offset..], libc::IFA_LOCAL, addr);
526        }
527
528        netlink_send(&buf)
529    }
530
531    /// Sends a netlink RTM_NEWROUTE message for a default route.
532    fn netlink_newroute(family: u8, gateway: &[u8]) -> std::io::Result<()> {
533        let gw_len = gateway.len();
534
535        // nlmsghdr + rtmsg + RTA_GATEWAY(rta_header + addr)
536        let rta_len = rta_space(gw_len);
537        let msg_len = NLMSG_HDRLEN + RTMSG_LEN + rta_len;
538        let mut buf = vec![0u8; nlmsg_align(msg_len)];
539
540        // nlmsghdr
541        let nlh = buf.as_mut_ptr().cast::<libc::nlmsghdr>();
542        unsafe {
543            (*nlh).nlmsg_len = msg_len as u32;
544            (*nlh).nlmsg_type = libc::RTM_NEWROUTE;
545            (*nlh).nlmsg_flags =
546                (libc::NLM_F_REQUEST | libc::NLM_F_ACK | libc::NLM_F_CREATE | libc::NLM_F_EXCL)
547                    as u16;
548            (*nlh).nlmsg_seq = 2;
549        }
550
551        // rtmsg
552        let rtm = unsafe { buf.as_mut_ptr().add(NLMSG_HDRLEN).cast::<RtMsg>() };
553        unsafe {
554            (*rtm).rtm_family = family;
555            (*rtm).rtm_dst_len = 0; // default route
556            (*rtm).rtm_src_len = 0;
557            (*rtm).rtm_tos = 0;
558            (*rtm).rtm_table = libc::RT_TABLE_MAIN;
559            (*rtm).rtm_protocol = libc::RTPROT_BOOT;
560            (*rtm).rtm_scope = libc::RT_SCOPE_UNIVERSE;
561            (*rtm).rtm_type = libc::RTN_UNICAST;
562            (*rtm).rtm_flags = 0;
563        }
564
565        // RTA_GATEWAY attribute
566        let rta_offset = NLMSG_HDRLEN + RTMSG_LEN;
567        write_rta(&mut buf[rta_offset..], libc::RTA_GATEWAY, gateway);
568
569        netlink_send(&buf)
570    }
571
572    /// Opens a netlink socket, sends a message, and waits for the ACK.
573    fn netlink_send(msg: &[u8]) -> std::io::Result<()> {
574        unsafe {
575            let sock = libc::socket(libc::AF_NETLINK, libc::SOCK_DGRAM, libc::NETLINK_ROUTE);
576            if sock < 0 {
577                return Err(std::io::Error::last_os_error());
578            }
579
580            // Bind to kernel.
581            let mut sa: libc::sockaddr_nl = std::mem::zeroed();
582            sa.nl_family = libc::AF_NETLINK as u16;
583            if libc::bind(
584                sock,
585                (&sa as *const libc::sockaddr_nl).cast(),
586                std::mem::size_of::<libc::sockaddr_nl>() as u32,
587            ) < 0
588            {
589                libc::close(sock);
590                return Err(std::io::Error::last_os_error());
591            }
592
593            // Send.
594            if libc::send(sock, msg.as_ptr().cast(), msg.len(), 0) < 0 {
595                libc::close(sock);
596                return Err(std::io::Error::last_os_error());
597            }
598
599            // Read ACK.
600            let mut ack_buf = [0u8; 1024];
601            let n = libc::recv(sock, ack_buf.as_mut_ptr().cast(), ack_buf.len(), 0);
602            libc::close(sock);
603
604            if n < 0 {
605                return Err(std::io::Error::last_os_error());
606            }
607
608            // Check for error in the ACK (using from_ne_bytes to avoid
609            // unaligned pointer dereference on the stack buffer).
610            if (n as usize) >= NLMSG_HDRLEN + 4 {
611                let nlh = ack_buf.as_ptr().cast::<libc::nlmsghdr>();
612                if (*nlh).nlmsg_type == libc::NLMSG_ERROR as u16 {
613                    let err = i32::from_ne_bytes(
614                        ack_buf[NLMSG_HDRLEN..NLMSG_HDRLEN + 4].try_into().unwrap(),
615                    );
616                    if err < 0 {
617                        return Err(std::io::Error::from_raw_os_error(-err));
618                    }
619                }
620            }
621
622            Ok(())
623        }
624    }
625
626    // ── resolv.conf ────────────────────────────────────────────────────
627
628    /// Writes `/etc/resolv.conf` with the configured DNS servers.
629    fn write_resolv_conf(dns_v4: Option<Ipv4Addr>, dns_v6: Option<Ipv6Addr>) -> AgentdResult<()> {
630        if dns_v4.is_none() && dns_v6.is_none() {
631            return Ok(());
632        }
633
634        let mut content = String::new();
635        if let Some(dns) = dns_v4 {
636            content.push_str(&format!("nameserver {dns}\n"));
637        }
638        if let Some(dns) = dns_v6 {
639            content.push_str(&format!("nameserver {dns}\n"));
640        }
641
642        std::fs::write("/etc/resolv.conf", &content)
643            .map_err(|e| AgentdError::Init(format!("failed to write /etc/resolv.conf: {e}")))?;
644
645        Ok(())
646    }
647
648    // ── low-level helpers ──────────────────────────────────────────────
649
650    /// Creates a UDP socket for ioctl operations.
651    fn socket_fd() -> AgentdResult<libc::c_int> {
652        let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) };
653        if fd < 0 {
654            return Err(AgentdError::Init(format!(
655                "failed to create socket: {}",
656                std::io::Error::last_os_error()
657            )));
658        }
659        Ok(fd)
660    }
661
662    /// Copies an interface name into an ifreq struct.
663    fn copy_ifname(ifr: &mut libc::ifreq, ifname: &str) -> AgentdResult<()> {
664        let bytes = ifname.as_bytes();
665        if bytes.len() >= libc::IFNAMSIZ {
666            return Err(AgentdError::Init(format!(
667                "interface name too long: {ifname}"
668            )));
669        }
670        unsafe {
671            std::ptr::copy_nonoverlapping(
672                bytes.as_ptr(),
673                ifr.ifr_name.as_mut_ptr().cast(),
674                bytes.len(),
675            );
676        }
677        Ok(())
678    }
679
680    // ── netlink constants and helpers ──────────────────────────────────
681
682    const NLMSG_HDRLEN: usize = 16;
683    const IFADDRMSG_LEN: usize = 8;
684    const RTMSG_LEN: usize = 12;
685    const RTA_HDRLEN: usize = 4;
686
687    // Compile-time assertions: catch layout mismatches across platforms.
688    const _: () = assert!(std::mem::size_of::<libc::nlmsghdr>() == NLMSG_HDRLEN);
689    const _: () = assert!(std::mem::size_of::<IfAddrMsg>() == IFADDRMSG_LEN);
690    const _: () = assert!(std::mem::size_of::<RtMsg>() == RTMSG_LEN);
691
692    fn nlmsg_align(len: usize) -> usize {
693        (len + 3) & !3
694    }
695
696    fn rta_space(data_len: usize) -> usize {
697        nlmsg_align(RTA_HDRLEN + data_len)
698    }
699
700    /// Writes an rtattr (type + data) into the buffer.
701    fn write_rta(buf: &mut [u8], rta_type: u16, data: &[u8]) {
702        let rta_len = (RTA_HDRLEN + data.len()) as u16;
703        buf[0..2].copy_from_slice(&rta_len.to_ne_bytes());
704        buf[2..4].copy_from_slice(&rta_type.to_ne_bytes());
705        buf[RTA_HDRLEN..RTA_HDRLEN + data.len()].copy_from_slice(data);
706    }
707}
708
709//--------------------------------------------------------------------------------------------------
710// Tests
711//--------------------------------------------------------------------------------------------------
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716
717    #[test]
718    fn test_parse_net_full() {
719        let spec = parse_net("iface=eth0,mac=02:5a:7b:13:01:02,mtu=1500").unwrap();
720        assert_eq!(spec.iface, "eth0");
721        assert_eq!(spec.mac, [0x02, 0x5a, 0x7b, 0x13, 0x01, 0x02]);
722        assert_eq!(spec.mtu, 1500);
723    }
724
725    #[test]
726    fn test_parse_net_default_mtu() {
727        let spec = parse_net("iface=eth0,mac=02:00:00:00:00:01").unwrap();
728        assert_eq!(spec.mtu, 1500);
729    }
730
731    #[test]
732    fn test_parse_net_missing_iface() {
733        assert!(parse_net("mac=02:00:00:00:00:01").is_err());
734    }
735
736    #[test]
737    fn test_parse_net_missing_mac() {
738        assert!(parse_net("iface=eth0").is_err());
739    }
740
741    #[test]
742    fn test_parse_net_unknown_option() {
743        assert!(parse_net("iface=eth0,mac=02:00:00:00:00:01,bogus=42").is_err());
744    }
745
746    #[test]
747    fn test_parse_net_ipv4() {
748        let spec = parse_net_ipv4("addr=100.96.1.2/30,gw=100.96.1.1,dns=100.96.1.1").unwrap();
749        assert_eq!(spec.address, Ipv4Addr::new(100, 96, 1, 2));
750        assert_eq!(spec.prefix_len, 30);
751        assert_eq!(spec.gateway, Ipv4Addr::new(100, 96, 1, 1));
752        assert_eq!(spec.dns, Some(Ipv4Addr::new(100, 96, 1, 1)));
753    }
754
755    #[test]
756    fn test_parse_net_ipv4_no_dns() {
757        let spec = parse_net_ipv4("addr=10.0.0.2/24,gw=10.0.0.1").unwrap();
758        assert_eq!(spec.dns, None);
759    }
760
761    #[test]
762    fn test_parse_net_ipv4_missing_addr() {
763        assert!(parse_net_ipv4("gw=10.0.0.1").is_err());
764    }
765
766    #[test]
767    fn test_parse_net_ipv6() {
768        let spec = parse_net_ipv6(
769            "addr=fd42:6d73:62:2a::2/64,gw=fd42:6d73:62:2a::1,dns=fd42:6d73:62:2a::1",
770        )
771        .unwrap();
772        assert_eq!(
773            spec.address,
774            "fd42:6d73:62:2a::2".parse::<Ipv6Addr>().unwrap()
775        );
776        assert_eq!(spec.prefix_len, 64);
777        assert_eq!(
778            spec.gateway,
779            "fd42:6d73:62:2a::1".parse::<Ipv6Addr>().unwrap()
780        );
781        assert!(spec.dns.is_some());
782    }
783
784    #[test]
785    fn test_parse_mac_valid() {
786        let mac = parse_mac("02:5a:7b:13:01:02").unwrap();
787        assert_eq!(mac, [0x02, 0x5a, 0x7b, 0x13, 0x01, 0x02]);
788    }
789
790    #[test]
791    fn test_parse_mac_invalid() {
792        assert!(parse_mac("02:5a:7b").is_err());
793        assert!(parse_mac("zz:00:00:00:00:00").is_err());
794    }
795
796    #[test]
797    fn test_parse_cidr_v4() {
798        let (addr, prefix) = parse_cidr_v4("100.96.1.2/30").unwrap();
799        assert_eq!(addr, Ipv4Addr::new(100, 96, 1, 2));
800        assert_eq!(prefix, 30);
801    }
802
803    #[test]
804    fn test_parse_cidr_v6() {
805        let (addr, prefix) = parse_cidr_v6("fd42:6d73:62:2a::2/64").unwrap();
806        assert_eq!(addr, "fd42:6d73:62:2a::2".parse::<Ipv6Addr>().unwrap());
807        assert_eq!(prefix, 64);
808    }
809}