1use std::net::{Ipv4Addr, Ipv6Addr};
7
8use crate::config::NetConfig;
9use crate::error::AgentdResult;
10
11pub(crate) fn apply_hostname(
34 hostname: Option<&str>,
35 host_alias: Option<&str>,
36 gateway_ipv4: Option<Ipv4Addr>,
37 gateway_ipv6: Option<Ipv6Addr>,
38) -> AgentdResult<()> {
39 linux::write_hosts_file(hostname, host_alias, gateway_ipv4, gateway_ipv6)?;
40
41 if let Some(name) = hostname {
42 linux::set_hostname(name)?;
43 }
44
45 Ok(())
46}
47
48pub(crate) fn apply_network_config(cfg: NetConfig<'_>) -> AgentdResult<()> {
66 linux::configure_loopback()?;
67
68 let Some(net) = cfg.net else {
69 return Ok(());
70 };
71
72 linux::configure_interface(net, cfg.ipv4, cfg.ipv6)
73}
74
75fn hosts_file_contents(
88 hostname: Option<&str>,
89 host_alias: Option<&str>,
90 gateway_ipv4: Option<Ipv4Addr>,
91 gateway_ipv6: Option<Ipv6Addr>,
92) -> String {
93 let mut s = String::new();
94
95 if let Some(name) = hostname {
97 s.push_str(&format!("127.0.0.1\tlocalhost {name}\n"));
98 s.push_str(&format!(
99 "::1\tlocalhost ip6-localhost ip6-loopback {name}\n"
100 ));
101 } else {
102 s.push_str("127.0.0.1\tlocalhost\n");
103 s.push_str("::1\tlocalhost ip6-localhost ip6-loopback\n");
104 }
105
106 if let Some(alias) = host_alias {
109 if let Some(gw_v4) = gateway_ipv4 {
110 s.push_str(&format!("{gw_v4}\t{alias}\n"));
111 }
112 if let Some(gw_v6) = gateway_ipv6 {
113 s.push_str(&format!("{gw_v6}\t{alias}\n"));
114 }
115 }
116
117 s.push_str("fe00::\tip6-localnet\n");
118 s.push_str("ff00::\tip6-mcastprefix\n");
119 s.push_str("ff02::1\tip6-allnodes\n");
120 s.push_str("ff02::2\tip6-allrouters\n");
121
122 s
123}
124
125mod linux {
130 use std::net::{Ipv4Addr, Ipv6Addr};
131 use std::os::unix::fs::PermissionsExt;
132 use std::{fs, io, mem, ptr};
133
134 use nix::unistd;
135
136 use crate::config::{NetIpv4Spec, NetIpv6Spec, NetSpec};
137 use crate::error::{AgentdError, AgentdResult};
138
139 const ETC_NETWORK_FILE_MODE: u32 = 0o644;
144
145 #[repr(C)]
153 struct IfAddrMsg {
154 ifa_family: u8,
155 ifa_prefixlen: u8,
156 ifa_flags: u8,
157 ifa_scope: u8,
158 ifa_index: u32,
159 }
160
161 #[repr(C)]
162 struct RtMsg {
163 rtm_family: u8,
164 rtm_dst_len: u8,
165 rtm_src_len: u8,
166 rtm_tos: u8,
167 rtm_table: u8,
168 rtm_protocol: u8,
169 rtm_scope: u8,
170 rtm_type: u8,
171 rtm_flags: u32,
172 }
173
174 pub fn configure_interface(
186 net: &NetSpec,
187 ipv4: Option<&NetIpv4Spec>,
188 ipv6: Option<&NetIpv6Spec>,
189 ) -> AgentdResult<()> {
190 let ifindex = get_ifindex(&net.iface)?;
191
192 set_mac_address(&net.iface, &net.mac)?;
193 set_mtu(&net.iface, net.mtu)?;
194
195 if let Some(v4) = ipv4 {
196 add_address_v4(ifindex, v4.address, v4.prefix_len)?;
197 }
198 if let Some(v6) = ipv6 {
199 add_address_v6(ifindex, v6.address, v6.prefix_len)?;
200 }
201
202 bring_interface_up(&net.iface)?;
203
204 if let Some(v4) = ipv4 {
205 add_default_route_v4(v4.gateway)?;
206 }
207 if let Some(v6) = ipv6 {
208 add_default_route_v6(v6.gateway)?;
209 }
210
211 write_resolv_conf(ipv4.and_then(|v| v.dns), ipv6.and_then(|v| v.dns))?;
212
213 Ok(())
214 }
215
216 pub fn configure_loopback() -> AgentdResult<()> {
218 let ifindex = get_ifindex("lo")?;
219
220 bring_interface_up("lo")?;
221 add_address_v4_if_missing(ifindex, Ipv4Addr::LOCALHOST, 8)?;
222 add_address_v6_if_missing(ifindex, Ipv6Addr::LOCALHOST, 128)?;
223
224 Ok(())
225 }
226
227 fn get_ifindex(ifname: &str) -> AgentdResult<u32> {
231 unsafe {
232 let mut ifr: libc::ifreq = mem::zeroed();
233 copy_ifname(&mut ifr, ifname)?;
234
235 let sock = socket_fd()?;
236 if libc::ioctl(sock, libc::SIOCGIFINDEX as _, &mut ifr) < 0 {
237 libc::close(sock);
238 return Err(AgentdError::Init(format!(
239 "SIOCGIFINDEX failed for {ifname}: {}",
240 io::Error::last_os_error()
241 )));
242 }
243 libc::close(sock);
244
245 Ok(ifr.ifr_ifru.ifru_ifindex as u32)
246 }
247 }
248
249 fn set_mac_address(ifname: &str, mac: &[u8; 6]) -> AgentdResult<()> {
251 unsafe {
252 let mut ifr: libc::ifreq = mem::zeroed();
253 copy_ifname(&mut ifr, ifname)?;
254
255 ifr.ifr_ifru.ifru_hwaddr.sa_family = libc::ARPHRD_ETHER;
256 ifr.ifr_ifru.ifru_hwaddr.sa_data[..6].copy_from_slice(&mac.map(|b| b as libc::c_char));
257
258 let sock = socket_fd()?;
259 if libc::ioctl(sock, libc::SIOCSIFHWADDR as _, &ifr) < 0 {
260 libc::close(sock);
261 return Err(AgentdError::Init(format!(
262 "SIOCSIFHWADDR failed for {ifname}: {}",
263 io::Error::last_os_error()
264 )));
265 }
266 libc::close(sock);
267 }
268 Ok(())
269 }
270
271 fn set_mtu(ifname: &str, mtu: u16) -> AgentdResult<()> {
273 unsafe {
274 let mut ifr: libc::ifreq = mem::zeroed();
275 copy_ifname(&mut ifr, ifname)?;
276 ifr.ifr_ifru.ifru_mtu = mtu as libc::c_int;
277
278 let sock = socket_fd()?;
279 if libc::ioctl(sock, libc::SIOCSIFMTU as _, &ifr) < 0 {
280 libc::close(sock);
281 return Err(AgentdError::Init(format!(
282 "SIOCSIFMTU failed for {ifname}: {}",
283 io::Error::last_os_error()
284 )));
285 }
286 libc::close(sock);
287 }
288 Ok(())
289 }
290
291 fn bring_interface_up(ifname: &str) -> AgentdResult<()> {
293 unsafe {
294 let mut ifr: libc::ifreq = mem::zeroed();
295 copy_ifname(&mut ifr, ifname)?;
296
297 let sock = socket_fd()?;
298
299 if libc::ioctl(sock, libc::SIOCGIFFLAGS as _, &mut ifr) < 0 {
301 libc::close(sock);
302 return Err(AgentdError::Init(format!(
303 "SIOCGIFFLAGS failed for {ifname}: {}",
304 io::Error::last_os_error()
305 )));
306 }
307
308 ifr.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short;
310
311 if libc::ioctl(sock, libc::SIOCSIFFLAGS as _, &ifr) < 0 {
312 libc::close(sock);
313 return Err(AgentdError::Init(format!(
314 "SIOCSIFFLAGS (UP) failed for {ifname}: {}",
315 io::Error::last_os_error()
316 )));
317 }
318 libc::close(sock);
319 }
320 Ok(())
321 }
322
323 fn add_address_v4(ifindex: u32, addr: Ipv4Addr, prefix_len: u8) -> AgentdResult<()> {
327 let addr_bytes = addr.octets();
328 netlink_newaddr(ifindex, libc::AF_INET as u8, prefix_len, &addr_bytes).map_err(|e| {
329 AgentdError::Init(format!(
330 "failed to add IPv4 address {addr}/{prefix_len}: {e}"
331 ))
332 })
333 }
334
335 fn add_address_v6(ifindex: u32, addr: Ipv6Addr, prefix_len: u8) -> AgentdResult<()> {
337 let addr_bytes = addr.octets();
338 netlink_newaddr(ifindex, libc::AF_INET6 as u8, prefix_len, &addr_bytes).map_err(|e| {
339 AgentdError::Init(format!(
340 "failed to add IPv6 address {addr}/{prefix_len}: {e}"
341 ))
342 })
343 }
344
345 fn add_address_v4_if_missing(ifindex: u32, addr: Ipv4Addr, prefix_len: u8) -> AgentdResult<()> {
347 let addr_bytes = addr.octets();
348 match netlink_newaddr(ifindex, libc::AF_INET as u8, prefix_len, &addr_bytes) {
349 Ok(()) => Ok(()),
350 Err(e) if e.raw_os_error() == Some(libc::EEXIST) => Ok(()),
351 Err(e) => Err(AgentdError::Init(format!(
352 "failed to add IPv4 address {addr}/{prefix_len}: {e}"
353 ))),
354 }
355 }
356
357 fn add_address_v6_if_missing(ifindex: u32, addr: Ipv6Addr, prefix_len: u8) -> AgentdResult<()> {
359 let addr_bytes = addr.octets();
360 match netlink_newaddr(ifindex, libc::AF_INET6 as u8, prefix_len, &addr_bytes) {
361 Ok(()) => Ok(()),
362 Err(e) if e.raw_os_error() == Some(libc::EEXIST) => Ok(()),
363 Err(e) => Err(AgentdError::Init(format!(
364 "failed to add IPv6 address {addr}/{prefix_len}: {e}"
365 ))),
366 }
367 }
368
369 fn add_default_route_v4(gateway: Ipv4Addr) -> AgentdResult<()> {
371 let gw_bytes = gateway.octets();
372 netlink_newroute(libc::AF_INET as u8, &gw_bytes).map_err(|e| {
373 AgentdError::Init(format!(
374 "failed to add IPv4 default route via {gateway}: {e}"
375 ))
376 })
377 }
378
379 fn add_default_route_v6(gateway: Ipv6Addr) -> AgentdResult<()> {
381 let gw_bytes = gateway.octets();
382 netlink_newroute(libc::AF_INET6 as u8, &gw_bytes).map_err(|e| {
383 AgentdError::Init(format!(
384 "failed to add IPv6 default route via {gateway}: {e}"
385 ))
386 })
387 }
388
389 fn netlink_newaddr(ifindex: u32, family: u8, prefix_len: u8, addr: &[u8]) -> io::Result<()> {
394 let addr_len = addr.len();
395 let is_ipv4 = family == libc::AF_INET as u8;
396
397 let num_rtas = if is_ipv4 { 2 } else { 1 };
399 let rtas_len = rta_space(addr_len) * num_rtas;
400 let msg_len = NLMSG_HDRLEN + IFADDRMSG_LEN + rtas_len;
401 let mut buf = vec![0u8; nlmsg_align(msg_len)];
402
403 let nlh = buf.as_mut_ptr().cast::<libc::nlmsghdr>();
405 unsafe {
406 (*nlh).nlmsg_len = msg_len as u32;
407 (*nlh).nlmsg_type = libc::RTM_NEWADDR;
408 (*nlh).nlmsg_flags =
409 (libc::NLM_F_REQUEST | libc::NLM_F_ACK | libc::NLM_F_CREATE | libc::NLM_F_EXCL)
410 as u16;
411 (*nlh).nlmsg_seq = 1;
412 }
413
414 let ifa = unsafe { buf.as_mut_ptr().add(NLMSG_HDRLEN).cast::<IfAddrMsg>() };
416 unsafe {
417 (*ifa).ifa_family = family;
418 (*ifa).ifa_prefixlen = prefix_len;
419 (*ifa).ifa_flags = if is_ipv4 { 0 } else { libc::IFA_F_NODAD as u8 };
420 (*ifa).ifa_index = ifindex;
421 (*ifa).ifa_scope = libc::RT_SCOPE_UNIVERSE;
422 }
423
424 let mut rta_offset = NLMSG_HDRLEN + IFADDRMSG_LEN;
426 write_rta(&mut buf[rta_offset..], libc::IFA_ADDRESS, addr);
427 rta_offset += rta_space(addr_len);
428
429 if is_ipv4 {
430 write_rta(&mut buf[rta_offset..], libc::IFA_LOCAL, addr);
431 }
432
433 netlink_send(&buf)
434 }
435
436 fn netlink_newroute(family: u8, gateway: &[u8]) -> io::Result<()> {
438 let gw_len = gateway.len();
439
440 let rta_len = rta_space(gw_len);
442 let msg_len = NLMSG_HDRLEN + RTMSG_LEN + rta_len;
443 let mut buf = vec![0u8; nlmsg_align(msg_len)];
444
445 let nlh = buf.as_mut_ptr().cast::<libc::nlmsghdr>();
447 unsafe {
448 (*nlh).nlmsg_len = msg_len as u32;
449 (*nlh).nlmsg_type = libc::RTM_NEWROUTE;
450 (*nlh).nlmsg_flags =
451 (libc::NLM_F_REQUEST | libc::NLM_F_ACK | libc::NLM_F_CREATE | libc::NLM_F_EXCL)
452 as u16;
453 (*nlh).nlmsg_seq = 2;
454 }
455
456 let rtm = unsafe { buf.as_mut_ptr().add(NLMSG_HDRLEN).cast::<RtMsg>() };
458 unsafe {
459 (*rtm).rtm_family = family;
460 (*rtm).rtm_dst_len = 0; (*rtm).rtm_src_len = 0;
462 (*rtm).rtm_tos = 0;
463 (*rtm).rtm_table = libc::RT_TABLE_MAIN;
464 (*rtm).rtm_protocol = libc::RTPROT_BOOT;
465 (*rtm).rtm_scope = libc::RT_SCOPE_UNIVERSE;
466 (*rtm).rtm_type = libc::RTN_UNICAST;
467 (*rtm).rtm_flags = 0;
468 }
469
470 let rta_offset = NLMSG_HDRLEN + RTMSG_LEN;
472 write_rta(&mut buf[rta_offset..], libc::RTA_GATEWAY, gateway);
473
474 netlink_send(&buf)
475 }
476
477 fn netlink_send(msg: &[u8]) -> io::Result<()> {
479 unsafe {
480 let sock = libc::socket(libc::AF_NETLINK, libc::SOCK_DGRAM, libc::NETLINK_ROUTE);
481 if sock < 0 {
482 return Err(io::Error::last_os_error());
483 }
484
485 let mut sa: libc::sockaddr_nl = mem::zeroed();
487 sa.nl_family = libc::AF_NETLINK as u16;
488 if libc::bind(
489 sock,
490 (&sa as *const libc::sockaddr_nl).cast(),
491 mem::size_of::<libc::sockaddr_nl>() as u32,
492 ) < 0
493 {
494 libc::close(sock);
495 return Err(io::Error::last_os_error());
496 }
497
498 if libc::send(sock, msg.as_ptr().cast(), msg.len(), 0) < 0 {
500 libc::close(sock);
501 return Err(io::Error::last_os_error());
502 }
503
504 let mut ack_buf = [0u8; 1024];
506 let n = libc::recv(sock, ack_buf.as_mut_ptr().cast(), ack_buf.len(), 0);
507 libc::close(sock);
508
509 if n < 0 {
510 return Err(io::Error::last_os_error());
511 }
512
513 if (n as usize) >= NLMSG_HDRLEN + 4 {
516 let nlh = ack_buf.as_ptr().cast::<libc::nlmsghdr>();
517 if (*nlh).nlmsg_type == libc::NLMSG_ERROR as u16 {
518 let err = i32::from_ne_bytes(
519 ack_buf[NLMSG_HDRLEN..NLMSG_HDRLEN + 4].try_into().unwrap(),
520 );
521 if err < 0 {
522 return Err(io::Error::from_raw_os_error(-err));
523 }
524 }
525 }
526
527 Ok(())
528 }
529 }
530
531 pub fn set_hostname(name: &str) -> AgentdResult<()> {
535 unistd::sethostname(name)
536 .map_err(|e| AgentdError::Init(format!("sethostname({name}): {e}")))?;
537
538 fs::create_dir_all("/etc")
539 .map_err(|e| AgentdError::Init(format!("failed to create /etc: {e}")))?;
540 fs::write("/etc/hostname", format!("{name}\n"))
541 .map_err(|e| AgentdError::Init(format!("failed to write /etc/hostname: {e}")))?;
542
543 Ok(())
544 }
545
546 pub fn write_hosts_file(
548 hostname: Option<&str>,
549 host_alias: Option<&str>,
550 gateway_ipv4: Option<Ipv4Addr>,
551 gateway_ipv6: Option<Ipv6Addr>,
552 ) -> AgentdResult<()> {
553 fs::create_dir_all("/etc")
554 .map_err(|e| AgentdError::Init(format!("failed to create /etc: {e}")))?;
555 fs::write(
556 "/etc/hosts",
557 super::hosts_file_contents(hostname, host_alias, gateway_ipv4, gateway_ipv6),
558 )
559 .map_err(|e| AgentdError::Init(format!("failed to write /etc/hosts: {e}")))?;
560 fs::set_permissions(
561 "/etc/hosts",
562 fs::Permissions::from_mode(ETC_NETWORK_FILE_MODE),
563 )
564 .map_err(|e| AgentdError::Init(format!("failed to chmod /etc/hosts: {e}")))?;
565 Ok(())
566 }
567
568 fn write_resolv_conf(dns_v4: Option<Ipv4Addr>, dns_v6: Option<Ipv6Addr>) -> AgentdResult<()> {
570 if dns_v4.is_none() && dns_v6.is_none() {
571 return Ok(());
572 }
573
574 let mut content = String::new();
575 if let Some(dns) = dns_v4 {
576 content.push_str(&format!("nameserver {dns}\n"));
577 }
578 if let Some(dns) = dns_v6 {
579 content.push_str(&format!("nameserver {dns}\n"));
580 }
581
582 fs::write("/etc/resolv.conf", &content)
583 .map_err(|e| AgentdError::Init(format!("failed to write /etc/resolv.conf: {e}")))?;
584 fs::set_permissions(
585 "/etc/resolv.conf",
586 fs::Permissions::from_mode(ETC_NETWORK_FILE_MODE),
587 )
588 .map_err(|e| AgentdError::Init(format!("failed to chmod /etc/resolv.conf: {e}")))?;
589
590 Ok(())
591 }
592
593 fn socket_fd() -> AgentdResult<libc::c_int> {
597 let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) };
598 if fd < 0 {
599 return Err(AgentdError::Init(format!(
600 "failed to create socket: {}",
601 io::Error::last_os_error()
602 )));
603 }
604 Ok(fd)
605 }
606
607 fn copy_ifname(ifr: &mut libc::ifreq, ifname: &str) -> AgentdResult<()> {
609 let bytes = ifname.as_bytes();
610 if bytes.len() >= libc::IFNAMSIZ {
611 return Err(AgentdError::Init(format!(
612 "interface name too long: {ifname}"
613 )));
614 }
615 unsafe {
616 ptr::copy_nonoverlapping(
617 bytes.as_ptr(),
618 ifr.ifr_name.as_mut_ptr().cast(),
619 bytes.len(),
620 );
621 }
622 Ok(())
623 }
624
625 const NLMSG_HDRLEN: usize = 16;
628 const IFADDRMSG_LEN: usize = 8;
629 const RTMSG_LEN: usize = 12;
630 const RTA_HDRLEN: usize = 4;
631
632 const _: () = assert!(mem::size_of::<libc::nlmsghdr>() == NLMSG_HDRLEN);
634 const _: () = assert!(mem::size_of::<IfAddrMsg>() == IFADDRMSG_LEN);
635 const _: () = assert!(mem::size_of::<RtMsg>() == RTMSG_LEN);
636
637 fn nlmsg_align(len: usize) -> usize {
638 (len + 3) & !3
639 }
640
641 fn rta_space(data_len: usize) -> usize {
642 nlmsg_align(RTA_HDRLEN + data_len)
643 }
644
645 fn write_rta(buf: &mut [u8], rta_type: u16, data: &[u8]) {
647 let rta_len = (RTA_HDRLEN + data.len()) as u16;
648 buf[0..2].copy_from_slice(&rta_len.to_ne_bytes());
649 buf[2..4].copy_from_slice(&rta_type.to_ne_bytes());
650 buf[RTA_HDRLEN..RTA_HDRLEN + data.len()].copy_from_slice(data);
651 }
652}
653
654#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn test_hosts_file_without_hostname() {
664 assert_eq!(
665 hosts_file_contents(None, None, None, None),
666 concat!(
667 "127.0.0.1\tlocalhost\n",
668 "::1\tlocalhost ip6-localhost ip6-loopback\n",
669 "fe00::\tip6-localnet\n",
670 "ff00::\tip6-mcastprefix\n",
671 "ff02::1\tip6-allnodes\n",
672 "ff02::2\tip6-allrouters\n",
673 )
674 );
675 }
676
677 #[test]
678 fn test_hosts_file_with_hostname() {
679 assert_eq!(
680 hosts_file_contents(Some("worker-01"), None, None, None),
681 concat!(
682 "127.0.0.1\tlocalhost worker-01\n",
683 "::1\tlocalhost ip6-localhost ip6-loopback worker-01\n",
684 "fe00::\tip6-localnet\n",
685 "ff00::\tip6-mcastprefix\n",
686 "ff02::1\tip6-allnodes\n",
687 "ff02::2\tip6-allrouters\n",
688 )
689 );
690 }
691
692 #[test]
693 fn test_hosts_file_with_host_alias_both_families() {
694 assert_eq!(
695 hosts_file_contents(
696 Some("worker-01"),
697 Some("host.microsandbox.internal"),
698 Some(Ipv4Addr::new(100, 96, 0, 1)),
699 Some("fd42:6d73:62::1".parse().unwrap()),
700 ),
701 concat!(
702 "127.0.0.1\tlocalhost worker-01\n",
703 "::1\tlocalhost ip6-localhost ip6-loopback worker-01\n",
704 "100.96.0.1\thost.microsandbox.internal\n",
705 "fd42:6d73:62::1\thost.microsandbox.internal\n",
706 "fe00::\tip6-localnet\n",
707 "ff00::\tip6-mcastprefix\n",
708 "ff02::1\tip6-allnodes\n",
709 "ff02::2\tip6-allrouters\n",
710 )
711 );
712 }
713
714 #[test]
715 fn test_hosts_file_with_host_alias_v4_only() {
716 let out = hosts_file_contents(
717 None,
718 Some("host.microsandbox.internal"),
719 Some(Ipv4Addr::new(100, 96, 0, 1)),
720 None,
721 );
722 assert!(out.contains("100.96.0.1\thost.microsandbox.internal\n"));
723 assert!(!out.contains("fd42"));
724 }
725
726 #[test]
727 fn test_hosts_file_omits_alias_when_name_missing() {
728 let out = hosts_file_contents(
729 None,
730 None,
731 Some(Ipv4Addr::new(100, 96, 0, 1)),
732 Some("fd42:6d73:62::1".parse().unwrap()),
733 );
734 assert!(!out.contains("host.microsandbox.internal"));
735 assert!(!out.contains("100.96.0.1"));
736 assert!(!out.contains("fd42"));
737 }
738}