diff --git a/Cargo.lock b/Cargo.lock index 0a311b13a9..db46b1c011 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1057,6 +1057,7 @@ dependencies = [ "carbide-rpc", "carbide-rpc-utils", "carbide-systemd", + "carbide-test-support", "carbide-tls", "carbide-utils", "carbide-uuid", diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index b92b232070..cf315b01db 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -131,6 +131,7 @@ carbide-version = { path = "../version" } tonic-prost-build = { workspace = true } [dev-dependencies] +carbide-test-support = { path = "../test-support" } ctor = { workspace = true } prost = { workspace = true } rustls-pemfile = { workspace = true } diff --git a/crates/agent/src/dhcp.rs b/crates/agent/src/dhcp.rs index fb0bbde91d..fd37dad3f8 100644 --- a/crates/agent/src/dhcp.rs +++ b/crates/agent/src/dhcp.rs @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -use std::net::Ipv4Addr; +use std::net::{Ipv4Addr, Ipv6Addr}; use ::rpc::forge as rpc; use carbide_rpc_utils::dhcp::HostConfig; @@ -64,12 +64,14 @@ pub fn build_server_config( pxe_ip: Ipv4Addr, ntpservers: Vec, nameservers: Vec, + nameservers_v6: Vec, loopback_ip: Ipv4Addr, ) -> Result { let dhcp_config = carbide_rpc_utils::dhcp::DhcpConfig::from_forge_dhcp_config( pxe_ip, ntpservers, nameservers, + nameservers_v6, loopback_ip, )?; diff --git a/crates/agent/src/ethernet_virtualization.rs b/crates/agent/src/ethernet_virtualization.rs index 9bba200fe2..73a8c671ff 100644 --- a/crates/agent/src/ethernet_virtualization.rs +++ b/crates/agent/src/ethernet_virtualization.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; use std::ffi::CStr; use std::fs::File; use std::io::Read; -use std::net::{IpAddr, Ipv4Addr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; @@ -138,6 +138,21 @@ pub struct ServiceAddresses { pub nameservers: Vec, } +/// Split a dual-stack nameserver list into its IPv4 and IPv6 members, so the +/// gRPC and file-write DHCP-config paths derive both families the same way. +fn split_nameservers_by_family(nameservers: &[IpAddr]) -> (Vec, Vec) { + nameservers + .iter() + .copied() + .fold((Vec::new(), Vec::new()), |(mut v4, mut v6), addr| { + match addr { + IpAddr::V4(v4_addr) => v4.push(v4_addr), + IpAddr::V6(v6_addr) => v6.push(v6_addr), + } + (v4, v6) + }) +} + fn build_dhcp_ntp_servers( nc: &rpc::ManagedHostNetworkConfigResponse, service_addrs: &ServiceAddresses, @@ -1066,14 +1081,7 @@ async fn update_dhcp_via_grpc( }; let loopback_ip: Ipv4Addr = mh_nc.loopback_ip.parse()?; - let nameservers_v4 = service_addrs - .nameservers - .iter() - .filter_map(|x| match x { - IpAddr::V4(x) => Some(*x), - _ => None, - }) - .collect::>(); + let (nameservers_v4, nameservers_v6) = split_nameservers_by_family(&service_addrs.nameservers); let ntpservers_v4 = build_dhcp_ntp_servers(network_config, service_addrs); @@ -1095,6 +1103,7 @@ async fn update_dhcp_via_grpc( pxe_ip_v4, ntpservers_v4, nameservers_v4, + nameservers_v6, loopback_ip, )?; let mut host_config = carbide_rpc_utils::dhcp::HostConfig::try_from( @@ -1471,18 +1480,10 @@ fn write_dhcp_v4_server_config( let loopback_ip = mh_nc.loopback_ip.parse()?; - // Filter to IPv4, since this is specifically for the DHCPv4 server - // config, and the input ServiceAddresses holds both families. - // Again, we'll eventually have a specific builder for a DHCPv6 - // that does similar things with ServiceAddresses, but for IPv6. - let nameservers_v4 = service_addrs - .nameservers - .iter() - .filter_map(|x| match x { - IpAddr::V4(x) => Some(*x), - _ => None, - }) - .collect::>(); + // Split the dual-stack nameservers by family: the IPv4 set drives the + // DHCPv4 options written here, while the IPv6 set is held in the config for + // the eventual DHCPv6 / RA consumer (inert in this path for now). + let (nameservers_v4, nameservers_v6) = split_nameservers_by_family(&service_addrs.nameservers); let ntpservers_v4 = build_dhcp_ntp_servers(nc, service_addrs); @@ -1517,8 +1518,13 @@ fn write_dhcp_v4_server_config( Err(err) => tracing::error!("Write DHCP server {}: {err:#}", dhcp_server_path.server), } - let next_contents = - dhcp::build_server_config(pxe_ip_v4, ntpservers_v4, nameservers_v4, loopback_ip)?; + let next_contents = dhcp::build_server_config( + pxe_ip_v4, + ntpservers_v4, + nameservers_v4, + nameservers_v6, + loopback_ip, + )?; match write( next_contents, &dhcp_server_path.config, @@ -1923,7 +1929,7 @@ impl InterfaceTranslationMode { mod tests { use std::fs; use std::io::Write; - use std::net::{IpAddr, Ipv4Addr}; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -3305,6 +3311,31 @@ mod tests { Ok(()) } + #[test] + fn split_nameservers_by_family_partitions_by_family() { + use carbide_test_support::value_scenarios; + + value_scenarios!( + run = |input: Vec| -> (Vec, Vec) { + split_nameservers_by_family(&input) + }; + "splits nameservers by family" { + vec![ + IpAddr::from([10, 0, 0, 1]), + "2001:db8::1".parse::().unwrap(), + IpAddr::from([10, 0, 0, 2]), + ] => ( + vec![Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 2)], + vec!["2001:db8::1".parse::().unwrap()], + ), + vec![IpAddr::from([10, 0, 0, 1])] => (vec![Ipv4Addr::new(10, 0, 0, 1)], vec![]), + vec!["2001:db8::1".parse::().unwrap()] + => (vec![], vec!["2001:db8::1".parse::().unwrap()]), + vec![] => (vec![], vec![]), + } + ); + } + fn validate_dhcp_config(received: DhcpConfig, expected: DhcpConfig) { assert_eq!(received.lease_time_secs, expected.lease_time_secs); assert_eq!(received.renewal_time_secs, expected.renewal_time_secs); diff --git a/crates/rpc-utils/src/dhcp.rs b/crates/rpc-utils/src/dhcp.rs index 1f3823955d..18fc883626 100644 --- a/crates/rpc-utils/src/dhcp.rs +++ b/crates/rpc-utils/src/dhcp.rs @@ -96,10 +96,12 @@ impl DhcpConfig { carbide_provisioning_server_ipv4: Ipv4Addr, carbide_ntpservers: Vec, carbide_nameservers: Vec, + carbide_nameservers_v6: Vec, loopback_ip: Ipv4Addr, ) -> Result { Ok(DhcpConfig { carbide_nameservers, + carbide_nameservers_v6, carbide_ntpservers, carbide_provisioning_server_ipv4, carbide_dhcp_server: loopback_ip, @@ -344,6 +346,7 @@ mod tests { dhcp_server: Ipv4Addr, ntpservers: Vec, nameservers: Vec, + nameservers_v6: Vec, lease_time_secs: u32, } @@ -434,10 +437,11 @@ mod tests { } fn summarize_dhcp_config( - (provisioning_server, ntpservers, nameservers, dhcp_server): ( + (provisioning_server, ntpservers, nameservers, nameservers_v6, dhcp_server): ( Ipv4Addr, Vec, Vec, + Vec, Ipv4Addr, ), ) -> Result { @@ -445,6 +449,7 @@ mod tests { provisioning_server, ntpservers, nameservers, + nameservers_v6, dhcp_server, ) .map(|config| DhcpConfigSummary { @@ -452,6 +457,7 @@ mod tests { dhcp_server: config.carbide_dhcp_server, ntpservers: config.carbide_ntpservers, nameservers: config.carbide_nameservers, + nameservers_v6: config.carbide_nameservers_v6, lease_time_secs: config.lease_time_secs, }) .map_err(dhcp_error_kind) @@ -484,12 +490,14 @@ mod tests { Ipv4Addr::new(192, 0, 2, 10), vec![Ipv4Addr::new(192, 0, 2, 20)], vec![Ipv4Addr::new(192, 0, 2, 53)], + vec!["2001:db8::53".parse::().unwrap()], Ipv4Addr::new(127, 0, 0, 2), ) => Yields(DhcpConfigSummary { provisioning_server: Ipv4Addr::new(192, 0, 2, 10), dhcp_server: Ipv4Addr::new(127, 0, 0, 2), ntpservers: vec![Ipv4Addr::new(192, 0, 2, 20)], nameservers: vec![Ipv4Addr::new(192, 0, 2, 53)], + nameservers_v6: vec!["2001:db8::53".parse::().unwrap()], lease_time_secs: DEFAULT_LEASE_TIME_SECS, }), }