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::{fs, io, mem, ptr};
132
133 use nix::unistd;
134
135 use crate::config::{NetIpv4Spec, NetIpv6Spec, NetSpec};
136 use crate::error::{AgentdError, AgentdResult};
137
138 #[repr(C)]
146 struct IfAddrMsg {
147 ifa_family: u8,
148 ifa_prefixlen: u8,
149 ifa_flags: u8,
150 ifa_scope: u8,
151 ifa_index: u32,
152 }
153
154 #[repr(C)]
155 struct RtMsg {
156 rtm_family: u8,
157 rtm_dst_len: u8,
158 rtm_src_len: u8,
159 rtm_tos: u8,
160 rtm_table: u8,
161 rtm_protocol: u8,
162 rtm_scope: u8,
163 rtm_type: u8,
164 rtm_flags: u32,
165 }
166
167 pub fn configure_interface(
179 net: &NetSpec,
180 ipv4: Option<&NetIpv4Spec>,
181 ipv6: Option<&NetIpv6Spec>,
182 ) -> AgentdResult<()> {
183 let ifindex = get_ifindex(&net.iface)?;
184
185 set_mac_address(&net.iface, &net.mac)?;
186 set_mtu(&net.iface, net.mtu)?;
187
188 if let Some(v4) = ipv4 {
189 add_address_v4(ifindex, v4.address, v4.prefix_len)?;
190 }
191 if let Some(v6) = ipv6 {
192 add_address_v6(ifindex, v6.address, v6.prefix_len)?;
193 }
194
195 bring_interface_up(&net.iface)?;
196
197 if let Some(v4) = ipv4 {
198 add_default_route_v4(v4.gateway)?;
199 }
200 if let Some(v6) = ipv6 {
201 add_default_route_v6(v6.gateway)?;
202 }
203
204 write_resolv_conf(ipv4.and_then(|v| v.dns), ipv6.and_then(|v| v.dns))?;
205
206 Ok(())
207 }
208
209 pub fn configure_loopback() -> AgentdResult<()> {
211 let ifindex = get_ifindex("lo")?;
212
213 bring_interface_up("lo")?;
214 add_address_v4_if_missing(ifindex, Ipv4Addr::LOCALHOST, 8)?;
215 add_address_v6_if_missing(ifindex, Ipv6Addr::LOCALHOST, 128)?;
216
217 Ok(())
218 }
219
220 fn get_ifindex(ifname: &str) -> AgentdResult<u32> {
224 unsafe {
225 let mut ifr: libc::ifreq = mem::zeroed();
226 copy_ifname(&mut ifr, ifname)?;
227
228 let sock = socket_fd()?;
229 if libc::ioctl(sock, libc::SIOCGIFINDEX as _, &mut ifr) < 0 {
230 libc::close(sock);
231 return Err(AgentdError::Init(format!(
232 "SIOCGIFINDEX failed for {ifname}: {}",
233 io::Error::last_os_error()
234 )));
235 }
236 libc::close(sock);
237
238 Ok(ifr.ifr_ifru.ifru_ifindex as u32)
239 }
240 }
241
242 fn set_mac_address(ifname: &str, mac: &[u8; 6]) -> AgentdResult<()> {
244 unsafe {
245 let mut ifr: libc::ifreq = mem::zeroed();
246 copy_ifname(&mut ifr, ifname)?;
247
248 ifr.ifr_ifru.ifru_hwaddr.sa_family = libc::ARPHRD_ETHER;
249 ifr.ifr_ifru.ifru_hwaddr.sa_data[..6].copy_from_slice(&mac.map(|b| b as libc::c_char));
250
251 let sock = socket_fd()?;
252 if libc::ioctl(sock, libc::SIOCSIFHWADDR as _, &ifr) < 0 {
253 libc::close(sock);
254 return Err(AgentdError::Init(format!(
255 "SIOCSIFHWADDR failed for {ifname}: {}",
256 io::Error::last_os_error()
257 )));
258 }
259 libc::close(sock);
260 }
261 Ok(())
262 }
263
264 fn set_mtu(ifname: &str, mtu: u16) -> AgentdResult<()> {
266 unsafe {
267 let mut ifr: libc::ifreq = mem::zeroed();
268 copy_ifname(&mut ifr, ifname)?;
269 ifr.ifr_ifru.ifru_mtu = mtu as libc::c_int;
270
271 let sock = socket_fd()?;
272 if libc::ioctl(sock, libc::SIOCSIFMTU as _, &ifr) < 0 {
273 libc::close(sock);
274 return Err(AgentdError::Init(format!(
275 "SIOCSIFMTU failed for {ifname}: {}",
276 io::Error::last_os_error()
277 )));
278 }
279 libc::close(sock);
280 }
281 Ok(())
282 }
283
284 fn bring_interface_up(ifname: &str) -> AgentdResult<()> {
286 unsafe {
287 let mut ifr: libc::ifreq = mem::zeroed();
288 copy_ifname(&mut ifr, ifname)?;
289
290 let sock = socket_fd()?;
291
292 if libc::ioctl(sock, libc::SIOCGIFFLAGS as _, &mut ifr) < 0 {
294 libc::close(sock);
295 return Err(AgentdError::Init(format!(
296 "SIOCGIFFLAGS failed for {ifname}: {}",
297 io::Error::last_os_error()
298 )));
299 }
300
301 ifr.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short;
303
304 if libc::ioctl(sock, libc::SIOCSIFFLAGS as _, &ifr) < 0 {
305 libc::close(sock);
306 return Err(AgentdError::Init(format!(
307 "SIOCSIFFLAGS (UP) failed for {ifname}: {}",
308 io::Error::last_os_error()
309 )));
310 }
311 libc::close(sock);
312 }
313 Ok(())
314 }
315
316 fn add_address_v4(ifindex: u32, addr: Ipv4Addr, prefix_len: u8) -> AgentdResult<()> {
320 let addr_bytes = addr.octets();
321 netlink_newaddr(ifindex, libc::AF_INET as u8, prefix_len, &addr_bytes).map_err(|e| {
322 AgentdError::Init(format!(
323 "failed to add IPv4 address {addr}/{prefix_len}: {e}"
324 ))
325 })
326 }
327
328 fn add_address_v6(ifindex: u32, addr: Ipv6Addr, prefix_len: u8) -> AgentdResult<()> {
330 let addr_bytes = addr.octets();
331 netlink_newaddr(ifindex, libc::AF_INET6 as u8, prefix_len, &addr_bytes).map_err(|e| {
332 AgentdError::Init(format!(
333 "failed to add IPv6 address {addr}/{prefix_len}: {e}"
334 ))
335 })
336 }
337
338 fn add_address_v4_if_missing(ifindex: u32, addr: Ipv4Addr, prefix_len: u8) -> AgentdResult<()> {
340 let addr_bytes = addr.octets();
341 match netlink_newaddr(ifindex, libc::AF_INET as u8, prefix_len, &addr_bytes) {
342 Ok(()) => Ok(()),
343 Err(e) if e.raw_os_error() == Some(libc::EEXIST) => Ok(()),
344 Err(e) => Err(AgentdError::Init(format!(
345 "failed to add IPv4 address {addr}/{prefix_len}: {e}"
346 ))),
347 }
348 }
349
350 fn add_address_v6_if_missing(ifindex: u32, addr: Ipv6Addr, prefix_len: u8) -> AgentdResult<()> {
352 let addr_bytes = addr.octets();
353 match netlink_newaddr(ifindex, libc::AF_INET6 as u8, prefix_len, &addr_bytes) {
354 Ok(()) => Ok(()),
355 Err(e) if e.raw_os_error() == Some(libc::EEXIST) => Ok(()),
356 Err(e) => Err(AgentdError::Init(format!(
357 "failed to add IPv6 address {addr}/{prefix_len}: {e}"
358 ))),
359 }
360 }
361
362 fn add_default_route_v4(gateway: Ipv4Addr) -> AgentdResult<()> {
364 let gw_bytes = gateway.octets();
365 netlink_newroute(libc::AF_INET as u8, &gw_bytes).map_err(|e| {
366 AgentdError::Init(format!(
367 "failed to add IPv4 default route via {gateway}: {e}"
368 ))
369 })
370 }
371
372 fn add_default_route_v6(gateway: Ipv6Addr) -> AgentdResult<()> {
374 let gw_bytes = gateway.octets();
375 netlink_newroute(libc::AF_INET6 as u8, &gw_bytes).map_err(|e| {
376 AgentdError::Init(format!(
377 "failed to add IPv6 default route via {gateway}: {e}"
378 ))
379 })
380 }
381
382 fn netlink_newaddr(ifindex: u32, family: u8, prefix_len: u8, addr: &[u8]) -> io::Result<()> {
387 let addr_len = addr.len();
388 let is_ipv4 = family == libc::AF_INET as u8;
389
390 let num_rtas = if is_ipv4 { 2 } else { 1 };
392 let rtas_len = rta_space(addr_len) * num_rtas;
393 let msg_len = NLMSG_HDRLEN + IFADDRMSG_LEN + rtas_len;
394 let mut buf = vec![0u8; nlmsg_align(msg_len)];
395
396 let nlh = buf.as_mut_ptr().cast::<libc::nlmsghdr>();
398 unsafe {
399 (*nlh).nlmsg_len = msg_len as u32;
400 (*nlh).nlmsg_type = libc::RTM_NEWADDR;
401 (*nlh).nlmsg_flags =
402 (libc::NLM_F_REQUEST | libc::NLM_F_ACK | libc::NLM_F_CREATE | libc::NLM_F_EXCL)
403 as u16;
404 (*nlh).nlmsg_seq = 1;
405 }
406
407 let ifa = unsafe { buf.as_mut_ptr().add(NLMSG_HDRLEN).cast::<IfAddrMsg>() };
409 unsafe {
410 (*ifa).ifa_family = family;
411 (*ifa).ifa_prefixlen = prefix_len;
412 (*ifa).ifa_flags = if is_ipv4 { 0 } else { libc::IFA_F_NODAD as u8 };
413 (*ifa).ifa_index = ifindex;
414 (*ifa).ifa_scope = libc::RT_SCOPE_UNIVERSE;
415 }
416
417 let mut rta_offset = NLMSG_HDRLEN + IFADDRMSG_LEN;
419 write_rta(&mut buf[rta_offset..], libc::IFA_ADDRESS, addr);
420 rta_offset += rta_space(addr_len);
421
422 if is_ipv4 {
423 write_rta(&mut buf[rta_offset..], libc::IFA_LOCAL, addr);
424 }
425
426 netlink_send(&buf)
427 }
428
429 fn netlink_newroute(family: u8, gateway: &[u8]) -> io::Result<()> {
431 let gw_len = gateway.len();
432
433 let rta_len = rta_space(gw_len);
435 let msg_len = NLMSG_HDRLEN + RTMSG_LEN + rta_len;
436 let mut buf = vec![0u8; nlmsg_align(msg_len)];
437
438 let nlh = buf.as_mut_ptr().cast::<libc::nlmsghdr>();
440 unsafe {
441 (*nlh).nlmsg_len = msg_len as u32;
442 (*nlh).nlmsg_type = libc::RTM_NEWROUTE;
443 (*nlh).nlmsg_flags =
444 (libc::NLM_F_REQUEST | libc::NLM_F_ACK | libc::NLM_F_CREATE | libc::NLM_F_EXCL)
445 as u16;
446 (*nlh).nlmsg_seq = 2;
447 }
448
449 let rtm = unsafe { buf.as_mut_ptr().add(NLMSG_HDRLEN).cast::<RtMsg>() };
451 unsafe {
452 (*rtm).rtm_family = family;
453 (*rtm).rtm_dst_len = 0; (*rtm).rtm_src_len = 0;
455 (*rtm).rtm_tos = 0;
456 (*rtm).rtm_table = libc::RT_TABLE_MAIN;
457 (*rtm).rtm_protocol = libc::RTPROT_BOOT;
458 (*rtm).rtm_scope = libc::RT_SCOPE_UNIVERSE;
459 (*rtm).rtm_type = libc::RTN_UNICAST;
460 (*rtm).rtm_flags = 0;
461 }
462
463 let rta_offset = NLMSG_HDRLEN + RTMSG_LEN;
465 write_rta(&mut buf[rta_offset..], libc::RTA_GATEWAY, gateway);
466
467 netlink_send(&buf)
468 }
469
470 fn netlink_send(msg: &[u8]) -> io::Result<()> {
472 unsafe {
473 let sock = libc::socket(libc::AF_NETLINK, libc::SOCK_DGRAM, libc::NETLINK_ROUTE);
474 if sock < 0 {
475 return Err(io::Error::last_os_error());
476 }
477
478 let mut sa: libc::sockaddr_nl = mem::zeroed();
480 sa.nl_family = libc::AF_NETLINK as u16;
481 if libc::bind(
482 sock,
483 (&sa as *const libc::sockaddr_nl).cast(),
484 mem::size_of::<libc::sockaddr_nl>() as u32,
485 ) < 0
486 {
487 libc::close(sock);
488 return Err(io::Error::last_os_error());
489 }
490
491 if libc::send(sock, msg.as_ptr().cast(), msg.len(), 0) < 0 {
493 libc::close(sock);
494 return Err(io::Error::last_os_error());
495 }
496
497 let mut ack_buf = [0u8; 1024];
499 let n = libc::recv(sock, ack_buf.as_mut_ptr().cast(), ack_buf.len(), 0);
500 libc::close(sock);
501
502 if n < 0 {
503 return Err(io::Error::last_os_error());
504 }
505
506 if (n as usize) >= NLMSG_HDRLEN + 4 {
509 let nlh = ack_buf.as_ptr().cast::<libc::nlmsghdr>();
510 if (*nlh).nlmsg_type == libc::NLMSG_ERROR as u16 {
511 let err = i32::from_ne_bytes(
512 ack_buf[NLMSG_HDRLEN..NLMSG_HDRLEN + 4].try_into().unwrap(),
513 );
514 if err < 0 {
515 return Err(io::Error::from_raw_os_error(-err));
516 }
517 }
518 }
519
520 Ok(())
521 }
522 }
523
524 pub fn set_hostname(name: &str) -> AgentdResult<()> {
528 unistd::sethostname(name)
529 .map_err(|e| AgentdError::Init(format!("sethostname({name}): {e}")))?;
530
531 fs::create_dir_all("/etc")
532 .map_err(|e| AgentdError::Init(format!("failed to create /etc: {e}")))?;
533 fs::write("/etc/hostname", format!("{name}\n"))
534 .map_err(|e| AgentdError::Init(format!("failed to write /etc/hostname: {e}")))?;
535
536 Ok(())
537 }
538
539 pub fn write_hosts_file(
541 hostname: Option<&str>,
542 host_alias: Option<&str>,
543 gateway_ipv4: Option<Ipv4Addr>,
544 gateway_ipv6: Option<Ipv6Addr>,
545 ) -> AgentdResult<()> {
546 fs::create_dir_all("/etc")
547 .map_err(|e| AgentdError::Init(format!("failed to create /etc: {e}")))?;
548 fs::write(
549 "/etc/hosts",
550 super::hosts_file_contents(hostname, host_alias, gateway_ipv4, gateway_ipv6),
551 )
552 .map_err(|e| AgentdError::Init(format!("failed to write /etc/hosts: {e}")))?;
553 Ok(())
554 }
555
556 fn write_resolv_conf(dns_v4: Option<Ipv4Addr>, dns_v6: Option<Ipv6Addr>) -> AgentdResult<()> {
558 if dns_v4.is_none() && dns_v6.is_none() {
559 return Ok(());
560 }
561
562 let mut content = String::new();
563 if let Some(dns) = dns_v4 {
564 content.push_str(&format!("nameserver {dns}\n"));
565 }
566 if let Some(dns) = dns_v6 {
567 content.push_str(&format!("nameserver {dns}\n"));
568 }
569
570 fs::write("/etc/resolv.conf", &content)
571 .map_err(|e| AgentdError::Init(format!("failed to write /etc/resolv.conf: {e}")))?;
572
573 Ok(())
574 }
575
576 fn socket_fd() -> AgentdResult<libc::c_int> {
580 let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) };
581 if fd < 0 {
582 return Err(AgentdError::Init(format!(
583 "failed to create socket: {}",
584 io::Error::last_os_error()
585 )));
586 }
587 Ok(fd)
588 }
589
590 fn copy_ifname(ifr: &mut libc::ifreq, ifname: &str) -> AgentdResult<()> {
592 let bytes = ifname.as_bytes();
593 if bytes.len() >= libc::IFNAMSIZ {
594 return Err(AgentdError::Init(format!(
595 "interface name too long: {ifname}"
596 )));
597 }
598 unsafe {
599 ptr::copy_nonoverlapping(
600 bytes.as_ptr(),
601 ifr.ifr_name.as_mut_ptr().cast(),
602 bytes.len(),
603 );
604 }
605 Ok(())
606 }
607
608 const NLMSG_HDRLEN: usize = 16;
611 const IFADDRMSG_LEN: usize = 8;
612 const RTMSG_LEN: usize = 12;
613 const RTA_HDRLEN: usize = 4;
614
615 const _: () = assert!(mem::size_of::<libc::nlmsghdr>() == NLMSG_HDRLEN);
617 const _: () = assert!(mem::size_of::<IfAddrMsg>() == IFADDRMSG_LEN);
618 const _: () = assert!(mem::size_of::<RtMsg>() == RTMSG_LEN);
619
620 fn nlmsg_align(len: usize) -> usize {
621 (len + 3) & !3
622 }
623
624 fn rta_space(data_len: usize) -> usize {
625 nlmsg_align(RTA_HDRLEN + data_len)
626 }
627
628 fn write_rta(buf: &mut [u8], rta_type: u16, data: &[u8]) {
630 let rta_len = (RTA_HDRLEN + data.len()) as u16;
631 buf[0..2].copy_from_slice(&rta_len.to_ne_bytes());
632 buf[2..4].copy_from_slice(&rta_type.to_ne_bytes());
633 buf[RTA_HDRLEN..RTA_HDRLEN + data.len()].copy_from_slice(data);
634 }
635}
636
637#[cfg(test)]
642mod tests {
643 use super::*;
644
645 #[test]
646 fn test_hosts_file_without_hostname() {
647 assert_eq!(
648 hosts_file_contents(None, None, None, None),
649 concat!(
650 "127.0.0.1\tlocalhost\n",
651 "::1\tlocalhost ip6-localhost ip6-loopback\n",
652 "fe00::\tip6-localnet\n",
653 "ff00::\tip6-mcastprefix\n",
654 "ff02::1\tip6-allnodes\n",
655 "ff02::2\tip6-allrouters\n",
656 )
657 );
658 }
659
660 #[test]
661 fn test_hosts_file_with_hostname() {
662 assert_eq!(
663 hosts_file_contents(Some("worker-01"), None, None, None),
664 concat!(
665 "127.0.0.1\tlocalhost worker-01\n",
666 "::1\tlocalhost ip6-localhost ip6-loopback worker-01\n",
667 "fe00::\tip6-localnet\n",
668 "ff00::\tip6-mcastprefix\n",
669 "ff02::1\tip6-allnodes\n",
670 "ff02::2\tip6-allrouters\n",
671 )
672 );
673 }
674
675 #[test]
676 fn test_hosts_file_with_host_alias_both_families() {
677 assert_eq!(
678 hosts_file_contents(
679 Some("worker-01"),
680 Some("host.microsandbox.internal"),
681 Some(Ipv4Addr::new(100, 96, 0, 1)),
682 Some("fd42:6d73:62::1".parse().unwrap()),
683 ),
684 concat!(
685 "127.0.0.1\tlocalhost worker-01\n",
686 "::1\tlocalhost ip6-localhost ip6-loopback worker-01\n",
687 "100.96.0.1\thost.microsandbox.internal\n",
688 "fd42:6d73:62::1\thost.microsandbox.internal\n",
689 "fe00::\tip6-localnet\n",
690 "ff00::\tip6-mcastprefix\n",
691 "ff02::1\tip6-allnodes\n",
692 "ff02::2\tip6-allrouters\n",
693 )
694 );
695 }
696
697 #[test]
698 fn test_hosts_file_with_host_alias_v4_only() {
699 let out = hosts_file_contents(
700 None,
701 Some("host.microsandbox.internal"),
702 Some(Ipv4Addr::new(100, 96, 0, 1)),
703 None,
704 );
705 assert!(out.contains("100.96.0.1\thost.microsandbox.internal\n"));
706 assert!(!out.contains("fd42"));
707 }
708
709 #[test]
710 fn test_hosts_file_omits_alias_when_name_missing() {
711 let out = hosts_file_contents(
712 None,
713 None,
714 Some(Ipv4Addr::new(100, 96, 0, 1)),
715 Some("fd42:6d73:62::1".parse().unwrap()),
716 );
717 assert!(!out.contains("host.microsandbox.internal"));
718 assert!(!out.contains("100.96.0.1"));
719 assert!(!out.contains("fd42"));
720 }
721}